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 +16 −4 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 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 +121 −57 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): """ 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.) class DistanceRatio(object): 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 __init__(self, matches): self._action_stack = deque(maxlen=10) self._current_action_stack = 0 self._observers = set() self.matches = matches self.mask = None @property def nvalid(self): return self.mask.sum() def distance_ratio(matches, ratio=0.8): def compute(self, ratio, mask=None, mask_name=None): """ Compute and return a mask for a matches dataframe using Lowe's ratio test. Loading @@ -33,12 +27,6 @@ def distance_ratio(matches, ratio=0.8): 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 Loading @@ -51,35 +39,111 @@ def distance_ratio(matches, ratio=0.8): default, since the ratio test will not work for them. """ 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 def func(group): res = [False] * len(group) if len(res) == 1: return res if group.iloc[0] < group.iloc[1] * ratio: res[0] = True return res 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 } 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 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']) # 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']) # 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 return mask def mirroring_test(matches): Loading @@ -104,8 +168,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 +278,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 +16 −4 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
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 +121 −57 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): """ 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.) class DistanceRatio(object): 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 __init__(self, matches): self._action_stack = deque(maxlen=10) self._current_action_stack = 0 self._observers = set() self.matches = matches self.mask = None @property def nvalid(self): return self.mask.sum() def distance_ratio(matches, ratio=0.8): def compute(self, ratio, mask=None, mask_name=None): """ Compute and return a mask for a matches dataframe using Lowe's ratio test. Loading @@ -33,12 +27,6 @@ def distance_ratio(matches, ratio=0.8): 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 Loading @@ -51,35 +39,111 @@ def distance_ratio(matches, ratio=0.8): default, since the ratio test will not work for them. """ 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 def func(group): res = [False] * len(group) if len(res) == 1: return res if group.iloc[0] < group.iloc[1] * ratio: res[0] = True return res 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 } 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 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']) # 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']) # 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 return mask def mirroring_test(matches): Loading @@ -104,8 +168,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 +278,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