Loading autocnet/examples/Apollo15/three_image_adjacency.json +2 −2 Original line number Diff line number Diff line {"AS15-M-0297_SML.png": ["AS15-M-0298_SML.png", "AS15-M-0299_SML.png"], "AS15-M-0298_SML.png": ["AS15-M-0297_SML.png", "AS15-M-0299_SML.png"], "AS15-M-0299_SML.png": ["AS15-M-0297_SML.png", "AS15-M-0298_SML.png"]} "AS15-M-0298_SML.png": ["AS15-M-0299_SML.png", "AS15-M-0297_SML.png"], "AS15-M-0299_SML.png": ["AS15-M-0298_SML.png", "AS15-M-0297_SML.png"]} autocnet/graph/edge.py +26 −7 Original line number Diff line number Diff line Loading @@ -54,10 +54,11 @@ class Edge(dict, MutableMapping): @property def masks(self): mask_lookup = {'fundamental': 'fundamental_matrix'} mask_lookup = {'fundamental': 'fundamental_matrix', 'ratio': 'distance_ratio'} if not hasattr(self, '_masks'): if hasattr(self, 'matches'): self._masks = pd.DataFrame(True, columns=['symmetry', 'ratio'], self._masks = pd.DataFrame(True, columns=['symmetry'], index=self.matches.index) else: self._masks = pd.DataFrame() Loading Loading @@ -110,9 +111,20 @@ class Edge(dict, MutableMapping): else: raise AttributeError('No matches have been computed for this edge.') def ratio_check(self, ratio=0.8): def ratio_check(self, ratio=0.8, clean_keys=[]): if hasattr(self, 'matches'): mask = od.distance_ratio(self.matches, ratio=ratio) if clean_keys: _, mask = self._clean(clean_keys) else: mask = pd.Series(True, self.matches.index) self.distance_ratio = od.DistanceRatio(self.matches) self.distance_ratio.compute(ratio, mask=mask, mask_name=None) # Setup to be notified self.distance_ratio._notify_subscribers(self.distance_ratio) self.masks = ('ratio', mask) else: raise AttributeError('No matches have been computed for this edge.') Loading Loading @@ -207,7 +219,7 @@ class Edge(dict, MutableMapping): def subpixel_register(self, clean_keys=[], threshold=0.8, upsampling=16, template_size=19, search_size=53, max_x_shift=1.0, max_y_shift=1.0): max_y_shift=1.0, tiled=False): """ For the entire graph, compute the subpixel offsets using pattern-matching and add the result as an attribute to each edge of the graph. Loading Loading @@ -252,6 +264,13 @@ class Edge(dict, MutableMapping): if clean_keys: matches, mask = self._clean(clean_keys) if tiled is True: s_img = self.source.handle d_img = self.destination.handle else: s_img = self.source.handle.read_array() d_img = self.destination.handle.read_array() # for each edge, calculate this for each keypoint pair for i, (idx, row) in enumerate(matches.iterrows()): s_idx = int(row['source_idx']) Loading @@ -261,8 +280,8 @@ class Edge(dict, MutableMapping): d_keypoint = self.destination.keypoints.iloc[d_idx][['x', 'y']].values # Get the template and search window s_template = sp.clip_roi(self.source.handle, s_keypoint, template_size) d_search = sp.clip_roi(self.destination.handle, d_keypoint, search_size) s_template = sp.clip_roi(s_img, s_keypoint, template_size) d_search = sp.clip_roi(d_img, d_keypoint, search_size) try: x_off, y_off, strength = sp.subpixel_offset(s_template, d_search, upsampling=upsampling) Loading autocnet/graph/network.py +12 −8 Original line number Diff line number Diff line Loading @@ -198,7 +198,7 @@ class CandidateGraph(nx.Graph): node.extract_features(image, method=method, extractor_parameters=extractor_parameters) def match_features(self, k=3): def match_features(self, k=None): """ For all connected edges in the graph, apply feature matching Loading @@ -207,18 +207,22 @@ class CandidateGraph(nx.Graph): 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 """ #Load a Fast Approximate Nearest Neighbor KD-Tree fl = FlannMatcher() degree = self.degree() self._fl = FlannMatcher() for i, node in self.nodes_iter(data=True): if not hasattr(node, 'descriptors'): raise AttributeError('Descriptors must be extracted before matching can occur.') fl.add(node.descriptors, key=i) fl.train() self._fl.add(node.descriptors, key=i) self._fl.train() for i, node in self.nodes_iter(data=True): if k is None: k = (degree[i] * 2) + 1 descriptors = node.descriptors matches = fl.query(descriptors, i, k=k) matches = self._fl.query(descriptors, i, k=k) self.add_matches(matches) def add_matches(self, matches): Loading Loading @@ -256,12 +260,12 @@ class CandidateGraph(nx.Graph): for s, d, edge in self.edges_iter(data=True): edge.symmetry_check() def ratio_checks(self, ratio=0.8): def ratio_checks(self, ratio=0.8, clean_keys=[]): """ Perform a ratio check on all edges in the graph """ for s, d, edge in self.edges_iter(data=True): edge.ratio_check(ratio=ratio) edge.ratio_check(ratio=ratio, clean_keys=clean_keys) def compute_homographies(self, clean_keys=[], **kwargs): """ Loading autocnet/matcher/outlier_detector.py +153 −60 Original line number Diff line number Diff line from collections import deque import cv2 import numpy as np import pandas as pd def self_neighbors(matches): class DistanceRatio(object): """ Returns a pandas data series intended to be used as a mask. Each row is True if it is not matched to a point in the same image (good) and False if it is (bad.) A stateful object to store ratio test results and provenance. Parameters Attributes ---------- nvalid : int The number of valid entries in the mask mask : series Pandas boolean series indexed by the match id matches : dataframe the matches dataframe stored along the edge of the graph containing matched points with columns containing: matched image name, query index, train index, and descriptor distance Returns ------- : dataseries Intended to mask the matches dataframe. True means the row is not matched to a point in the same image and false the row is. The matches dataframe from an edge. This dataframe must have 'source_idx' and 'distance' columns. single : bool If True, then single entries in the distance ratio mask are assumed to have passed the ratio test. Else False. """ return matches.source_image != matches.destination_image def __init__(self, matches): self._action_stack = deque(maxlen=10) self._current_action_stack = 0 self._observers = set() self.matches = matches self.mask = None def distance_ratio(matches, ratio=0.8): @property def nvalid(self): return self.mask.sum() def compute(self, ratio, mask=None, mask_name=None, single=False): """ Compute and return a mask for a matches dataframe using Lowe's ratio test. using Lowe's ratio test. If keypoints have a single Lowe (2004) [Lowe2004]_ Parameters ---------- matches : dataframe the matches dataframe stored along the edge of the graph containing matched points with columns containing: matched image name, query index, train index, and descriptor distance. ratio : float the ratio between the first and second-best match distances for each keypoint to use as a bound for marking the first keypoint as "good". Default: 0.8 Returns ------- mask : ndarray Intended to mask the matches dataframe. Rows are True if the associated keypoint passes the ratio test and false otherwise. Keypoints without more than one match are True by default, since the ratio test will not work for them. """ mask : series A pandas boolean series to initially mask the matches array mask = np.zeros(len(matches), dtype=bool) # Pre-allocate the mask counter = 0 for i, group in matches.groupby('source_idx'): group_size = len(group) n_unique = len(group['destination_idx'].unique()) # If we can not perform the ratio check because all matches are symmetrical if n_unique == 1: mask[counter:counter + group_size] = True counter += group_size else: # Otherwise, we can perform the ratio test sorted_group = group.sort_values(by=['distance']) unique = sorted_group['distance'].unique() if len(unique) == 1: # The distances from the unique points are identical mask[counter: counter + group_size] = False counter += group_size elif unique[0] / unique[1] < ratio: # The ratio test passes mask[counter] = True mask[counter + 1:counter + group_size] = False counter += group_size mask_name : list or str An arbitrary mask name for provenance tracking single : bool If True, points with only a single entry are included (True) in the result mask, else False. """ def func(group): res = [False] * len(group) if len(res) == 1: return [single] if group.iloc[0] < group.iloc[1] * ratio: res[0] = True return res self.single = single if mask is not None: self.mask = mask.copy() new_mask = self.matches[mask].groupby('source_idx')['distance'].transform(func).astype('bool') self.mask[mask==True] = new_mask else: mask[counter: counter + group_size] = False counter += group_size new_mask = self.matches.groupby('source_idx')['distance'].transform(func).astype('bool') self.mask = new_mask.copy() state_package = {'ratio': ratio, 'mask': self.mask.copy(), 'clean_keys': mask_name, 'single': single } self._action_stack.append(state_package) self._current_action_stack = len(self._action_stack) - 1 def subscribe(self, func): """ Subscribe some observer to the edge return mask Parameters ---------- func : object The callable that is to be executed on update """ self._observers.add(func) 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) def rollforward(self, n=1): """ Roll forwards in the object history, e.g. do Parameters ---------- n : int the number of steps to roll forwards """ 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): """ Roll backward in the object histroy, e.g. undo Parameters ---------- n : int the number of steps to roll backwards """ 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) self._notify_subscribers(self) def self_neighbors(matches): """ Returns a pandas data series intended to be used as a mask. Each row is True if it is not matched to a point in the same image (good) and False if it is (bad.) Parameters ---------- matches : dataframe the matches dataframe stored along the edge of the graph containing matched points with columns containing: matched image name, query index, train index, and descriptor distance Returns ------- : dataseries Intended to mask the matches dataframe. True means the row is not matched to a point in the same image and false the row is. """ return matches.source_image != matches.destination_image def mirroring_test(matches): Loading @@ -104,8 +197,7 @@ def mirroring_test(matches): otherwise, they will be false. Keypoints with only one match will be False. Removes duplicate rows. """ duplicates = matches.duplicated(keep='first').values duplicates.astype(bool, copy=False) duplicates = matches.duplicated(keep='first').astype(bool) return duplicates Loading Loading @@ -215,6 +307,7 @@ def compute_homography(kp1, kp2, method='ransac', **kwargs): kp2, method_, **kwargs) if mask is not None: mask = mask.astype(bool) return transformation_matrix, mask Loading autocnet/matcher/tests/test_outlier_detector.py +7 −4 Original line number Diff line number Diff line Loading @@ -36,10 +36,12 @@ class TestOutlierDetector(unittest.TestCase): counter += 1 fmatcher.train() self.matches = fmatcher.query(fd['AS15-M-0296_SML.png'][1],'AS15-M-0296_SML.png', k=3) self.matches = fmatcher.query(fd['AS15-M-0296_SML.png'][1], 'AS15-M-0296_SML.png') def test_distance_ratio(self): self.assertTrue(len(outlier_detector.distance_ratio(self.matches)), 13) d = outlier_detector.DistanceRatio(self.matches) d.compute(0.9) self.assertEqual(d.mask.sum(), 3) def test_distance_ratio_unique(self): data = [['A', 0, 'B', 1, 10], Loading @@ -47,8 +49,9 @@ class TestOutlierDetector(unittest.TestCase): df = pd.DataFrame(data, columns=['source_image', 'source_idx', 'destination_image', 'destination_idx', 'distance']) mask = outlier_detector.distance_ratio(df) self.assertTrue(mask.all() == False) d = outlier_detector.DistanceRatio(df) d.compute(0.9) self.assertTrue(d.mask.all() == False) def test_self_neighbors(self): # returned mask should be same length as input df Loading Loading
autocnet/examples/Apollo15/three_image_adjacency.json +2 −2 Original line number Diff line number Diff line {"AS15-M-0297_SML.png": ["AS15-M-0298_SML.png", "AS15-M-0299_SML.png"], "AS15-M-0298_SML.png": ["AS15-M-0297_SML.png", "AS15-M-0299_SML.png"], "AS15-M-0299_SML.png": ["AS15-M-0297_SML.png", "AS15-M-0298_SML.png"]} "AS15-M-0298_SML.png": ["AS15-M-0299_SML.png", "AS15-M-0297_SML.png"], "AS15-M-0299_SML.png": ["AS15-M-0298_SML.png", "AS15-M-0297_SML.png"]}
autocnet/graph/edge.py +26 −7 Original line number Diff line number Diff line Loading @@ -54,10 +54,11 @@ class Edge(dict, MutableMapping): @property def masks(self): mask_lookup = {'fundamental': 'fundamental_matrix'} mask_lookup = {'fundamental': 'fundamental_matrix', 'ratio': 'distance_ratio'} if not hasattr(self, '_masks'): if hasattr(self, 'matches'): self._masks = pd.DataFrame(True, columns=['symmetry', 'ratio'], self._masks = pd.DataFrame(True, columns=['symmetry'], index=self.matches.index) else: self._masks = pd.DataFrame() Loading Loading @@ -110,9 +111,20 @@ class Edge(dict, MutableMapping): else: raise AttributeError('No matches have been computed for this edge.') def ratio_check(self, ratio=0.8): def ratio_check(self, ratio=0.8, clean_keys=[]): if hasattr(self, 'matches'): mask = od.distance_ratio(self.matches, ratio=ratio) if clean_keys: _, mask = self._clean(clean_keys) else: mask = pd.Series(True, self.matches.index) self.distance_ratio = od.DistanceRatio(self.matches) self.distance_ratio.compute(ratio, mask=mask, mask_name=None) # Setup to be notified self.distance_ratio._notify_subscribers(self.distance_ratio) self.masks = ('ratio', mask) else: raise AttributeError('No matches have been computed for this edge.') Loading Loading @@ -207,7 +219,7 @@ class Edge(dict, MutableMapping): def subpixel_register(self, clean_keys=[], threshold=0.8, upsampling=16, template_size=19, search_size=53, max_x_shift=1.0, max_y_shift=1.0): max_y_shift=1.0, tiled=False): """ For the entire graph, compute the subpixel offsets using pattern-matching and add the result as an attribute to each edge of the graph. Loading Loading @@ -252,6 +264,13 @@ class Edge(dict, MutableMapping): if clean_keys: matches, mask = self._clean(clean_keys) if tiled is True: s_img = self.source.handle d_img = self.destination.handle else: s_img = self.source.handle.read_array() d_img = self.destination.handle.read_array() # for each edge, calculate this for each keypoint pair for i, (idx, row) in enumerate(matches.iterrows()): s_idx = int(row['source_idx']) Loading @@ -261,8 +280,8 @@ class Edge(dict, MutableMapping): d_keypoint = self.destination.keypoints.iloc[d_idx][['x', 'y']].values # Get the template and search window s_template = sp.clip_roi(self.source.handle, s_keypoint, template_size) d_search = sp.clip_roi(self.destination.handle, d_keypoint, search_size) s_template = sp.clip_roi(s_img, s_keypoint, template_size) d_search = sp.clip_roi(d_img, d_keypoint, search_size) try: x_off, y_off, strength = sp.subpixel_offset(s_template, d_search, upsampling=upsampling) Loading
autocnet/graph/network.py +12 −8 Original line number Diff line number Diff line Loading @@ -198,7 +198,7 @@ class CandidateGraph(nx.Graph): node.extract_features(image, method=method, extractor_parameters=extractor_parameters) def match_features(self, k=3): def match_features(self, k=None): """ For all connected edges in the graph, apply feature matching Loading @@ -207,18 +207,22 @@ class CandidateGraph(nx.Graph): 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 """ #Load a Fast Approximate Nearest Neighbor KD-Tree fl = FlannMatcher() degree = self.degree() self._fl = FlannMatcher() for i, node in self.nodes_iter(data=True): if not hasattr(node, 'descriptors'): raise AttributeError('Descriptors must be extracted before matching can occur.') fl.add(node.descriptors, key=i) fl.train() self._fl.add(node.descriptors, key=i) self._fl.train() for i, node in self.nodes_iter(data=True): if k is None: k = (degree[i] * 2) + 1 descriptors = node.descriptors matches = fl.query(descriptors, i, k=k) matches = self._fl.query(descriptors, i, k=k) self.add_matches(matches) def add_matches(self, matches): Loading Loading @@ -256,12 +260,12 @@ class CandidateGraph(nx.Graph): for s, d, edge in self.edges_iter(data=True): edge.symmetry_check() def ratio_checks(self, ratio=0.8): def ratio_checks(self, ratio=0.8, clean_keys=[]): """ Perform a ratio check on all edges in the graph """ for s, d, edge in self.edges_iter(data=True): edge.ratio_check(ratio=ratio) edge.ratio_check(ratio=ratio, clean_keys=clean_keys) def compute_homographies(self, clean_keys=[], **kwargs): """ Loading
autocnet/matcher/outlier_detector.py +153 −60 Original line number Diff line number Diff line from collections import deque import cv2 import numpy as np import pandas as pd def self_neighbors(matches): class DistanceRatio(object): """ Returns a pandas data series intended to be used as a mask. Each row is True if it is not matched to a point in the same image (good) and False if it is (bad.) A stateful object to store ratio test results and provenance. Parameters Attributes ---------- nvalid : int The number of valid entries in the mask mask : series Pandas boolean series indexed by the match id matches : dataframe the matches dataframe stored along the edge of the graph containing matched points with columns containing: matched image name, query index, train index, and descriptor distance Returns ------- : dataseries Intended to mask the matches dataframe. True means the row is not matched to a point in the same image and false the row is. The matches dataframe from an edge. This dataframe must have 'source_idx' and 'distance' columns. single : bool If True, then single entries in the distance ratio mask are assumed to have passed the ratio test. Else False. """ return matches.source_image != matches.destination_image def __init__(self, matches): self._action_stack = deque(maxlen=10) self._current_action_stack = 0 self._observers = set() self.matches = matches self.mask = None def distance_ratio(matches, ratio=0.8): @property def nvalid(self): return self.mask.sum() def compute(self, ratio, mask=None, mask_name=None, single=False): """ Compute and return a mask for a matches dataframe using Lowe's ratio test. using Lowe's ratio test. If keypoints have a single Lowe (2004) [Lowe2004]_ Parameters ---------- matches : dataframe the matches dataframe stored along the edge of the graph containing matched points with columns containing: matched image name, query index, train index, and descriptor distance. ratio : float the ratio between the first and second-best match distances for each keypoint to use as a bound for marking the first keypoint as "good". Default: 0.8 Returns ------- mask : ndarray Intended to mask the matches dataframe. Rows are True if the associated keypoint passes the ratio test and false otherwise. Keypoints without more than one match are True by default, since the ratio test will not work for them. """ mask : series A pandas boolean series to initially mask the matches array mask = np.zeros(len(matches), dtype=bool) # Pre-allocate the mask counter = 0 for i, group in matches.groupby('source_idx'): group_size = len(group) n_unique = len(group['destination_idx'].unique()) # If we can not perform the ratio check because all matches are symmetrical if n_unique == 1: mask[counter:counter + group_size] = True counter += group_size else: # Otherwise, we can perform the ratio test sorted_group = group.sort_values(by=['distance']) unique = sorted_group['distance'].unique() if len(unique) == 1: # The distances from the unique points are identical mask[counter: counter + group_size] = False counter += group_size elif unique[0] / unique[1] < ratio: # The ratio test passes mask[counter] = True mask[counter + 1:counter + group_size] = False counter += group_size mask_name : list or str An arbitrary mask name for provenance tracking single : bool If True, points with only a single entry are included (True) in the result mask, else False. """ def func(group): res = [False] * len(group) if len(res) == 1: return [single] if group.iloc[0] < group.iloc[1] * ratio: res[0] = True return res self.single = single if mask is not None: self.mask = mask.copy() new_mask = self.matches[mask].groupby('source_idx')['distance'].transform(func).astype('bool') self.mask[mask==True] = new_mask else: mask[counter: counter + group_size] = False counter += group_size new_mask = self.matches.groupby('source_idx')['distance'].transform(func).astype('bool') self.mask = new_mask.copy() state_package = {'ratio': ratio, 'mask': self.mask.copy(), 'clean_keys': mask_name, 'single': single } self._action_stack.append(state_package) self._current_action_stack = len(self._action_stack) - 1 def subscribe(self, func): """ Subscribe some observer to the edge return mask Parameters ---------- func : object The callable that is to be executed on update """ self._observers.add(func) 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) def rollforward(self, n=1): """ Roll forwards in the object history, e.g. do Parameters ---------- n : int the number of steps to roll forwards """ 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): """ Roll backward in the object histroy, e.g. undo Parameters ---------- n : int the number of steps to roll backwards """ 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) self._notify_subscribers(self) def self_neighbors(matches): """ Returns a pandas data series intended to be used as a mask. Each row is True if it is not matched to a point in the same image (good) and False if it is (bad.) Parameters ---------- matches : dataframe the matches dataframe stored along the edge of the graph containing matched points with columns containing: matched image name, query index, train index, and descriptor distance Returns ------- : dataseries Intended to mask the matches dataframe. True means the row is not matched to a point in the same image and false the row is. """ return matches.source_image != matches.destination_image def mirroring_test(matches): Loading @@ -104,8 +197,7 @@ def mirroring_test(matches): otherwise, they will be false. Keypoints with only one match will be False. Removes duplicate rows. """ duplicates = matches.duplicated(keep='first').values duplicates.astype(bool, copy=False) duplicates = matches.duplicated(keep='first').astype(bool) return duplicates Loading Loading @@ -215,6 +307,7 @@ def compute_homography(kp1, kp2, method='ransac', **kwargs): kp2, method_, **kwargs) if mask is not None: mask = mask.astype(bool) return transformation_matrix, mask Loading
autocnet/matcher/tests/test_outlier_detector.py +7 −4 Original line number Diff line number Diff line Loading @@ -36,10 +36,12 @@ class TestOutlierDetector(unittest.TestCase): counter += 1 fmatcher.train() self.matches = fmatcher.query(fd['AS15-M-0296_SML.png'][1],'AS15-M-0296_SML.png', k=3) self.matches = fmatcher.query(fd['AS15-M-0296_SML.png'][1], 'AS15-M-0296_SML.png') def test_distance_ratio(self): self.assertTrue(len(outlier_detector.distance_ratio(self.matches)), 13) d = outlier_detector.DistanceRatio(self.matches) d.compute(0.9) self.assertEqual(d.mask.sum(), 3) def test_distance_ratio_unique(self): data = [['A', 0, 'B', 1, 10], Loading @@ -47,8 +49,9 @@ class TestOutlierDetector(unittest.TestCase): df = pd.DataFrame(data, columns=['source_image', 'source_idx', 'destination_image', 'destination_idx', 'distance']) mask = outlier_detector.distance_ratio(df) self.assertTrue(mask.all() == False) d = outlier_detector.DistanceRatio(df) d.compute(0.9) self.assertTrue(d.mask.all() == False) def test_self_neighbors(self): # returned mask should be same length as input df Loading