Loading .travis.yml +1 −1 Original line number Diff line number Diff line Loading @@ -27,7 +27,7 @@ install: - conda info -a # Create a virtual env and install dependencies - conda create -y -q -n test-env python=$TRAVIS_PYTHON_VERSION nose gdal numpy pillow scipy pandas networkx - conda create -y -q -n test-env python=$TRAVIS_PYTHON_VERSION nose gdal numpy pillow scipy pandas networkx scikit-image # Activate the env - source activate test-env Loading autocnet/fileio/io_controlnetwork.py +0 −2 Original line number Diff line number Diff line Loading @@ -132,7 +132,6 @@ class IsisStore(object): """ point_sizes = [] point_messages = [] print(cnet) for pid, point in cnet.groupby('pid'): # Instantiate the proto spec point_spec = cnf.ControlPointFileEntryV0002() Loading @@ -146,7 +145,6 @@ class IsisStore(object): # A single extend call is cheaper than many add calls to pack points measure_iterable = [] print(point) for name, row in point.iterrows(): measure_spec = point_spec.Measure() measure_spec.serialnumber = row.nid Loading autocnet/graph/network.py +28 −4 Original line number Diff line number Diff line import operator import os import networkx as nx Loading Loading @@ -223,9 +224,17 @@ class CandidateGraph(nx.Graph): The identifier for the source node destination_key : str The identifier for the destination node outlier_algorithm : object An openCV outlier detections algorithm, e.g. cv2.RANSAC Returns ------- : tuple transformation_matrix : ndarray The 3x3 transformation matrix mask : ndarray Boolean array of the outliers A tuple of the form (transformation matrix, bad entry mask) The returned tuple is empty if there is no edge between the source and destination nodes or if it exists, but has not been populated with a matches dataframe. Loading @@ -250,14 +259,20 @@ class CandidateGraph(nx.Graph): source_keypoints.append(src_keypoint) destination_keypoints.append(dest_keypoint) return cv2.findHomography(np.array(source_keypoints), np.array(destination_keypoints), outlier_algorithm, 5.0) transformation_matrix, mask = cv2.findHomography(np.array(source_keypoints), np.array(destination_keypoints), outlier_algorithm, 5.0) mask = mask.astype(bool) return transformation_matrix, mask else: return ('', '') else: return ('','') def to_cnet(self): def to_cnet(self, clean_keys=[]): """ Generate a control network (C) object from a graph Loading @@ -265,11 +280,20 @@ class CandidateGraph(nx.Graph): ------- merged_cnet : C A control network object clean_keys : list of strings identifying the masking arrays to use, e.g. ratio, symmetry """ merged_cnet = None for source, destination, attributes in self.edges_iter(data=True): matches = attributes['matches'] # Merge all of the masks if clean_keys: mask = np.array(list(map(operator.mul, *[attributes[i] for i in clean_keys]))) matches = matches[mask] kp1 = self.node[source]['keypoints'] kp2 = self.node[destination]['keypoints'] Loading autocnet/matcher/matcher.py +75 −103 Original line number Diff line number Diff line import cv2 import numpy as np import pandas as pd from autocnet.graph.network import CandidateGraph import numpy as np from skimage.feature import match_template from scipy.misc import imresize FLANN_INDEX_KDTREE = 1 # Algorithm to set centers, DEFAULT_FLANN_PARAMETERS = dict(algorithm=FLANN_INDEX_KDTREE, trees=3) def pattern_match(template, image, upsampling=10, func=match_template): """ Call an arbitrary pattern matcher Parameters ---------- template : ndarray The input search template used to 'query' the destination image image : ndarray The image or sub-image to be searched upsampling : int The multiplier to upsample the template and image. func : object The function to be used to perform the template based matching Returns ------- x : float The x offset y : float The y offset strength : float The strength of the correlation in the range [-1, 1]. """ if upsampling < 1: raise ValueError u_template = imresize(template, (template.shape[0] * upsampling, template.shape[1] * upsampling), interp='bicubic') u_image = imresize(image, (image.shape[0] * upsampling, image.shape[1] * upsampling), interp='bicubic') # Find the the upper left origin of the template in the image match = func(u_image, u_template) y, x = np.unravel_index(np.argmax(match), match.shape) # Resample the match back to the native image resolution x /= upsampling y /= upsampling # Offset from the UL origin to the image center x += (template.shape[1] / 2) y += (template.shape[0] / 2) # Compute the offset to adjust the image match point location ideal_y = image.shape[0] / 2 ideal_x = image.shape[1] / 2 x = ideal_x - x y = ideal_y - y # Find the maximum correlation strength = np.max(match) return x, y, strength class FlannMatcher(object): """ A wrapper to the OpenCV Flann based matcher class that adds Loading Loading @@ -100,104 +173,3 @@ class FlannMatcher(object): return pd.DataFrame(matched, columns=['source_image', 'source_idx', 'destination_image', 'destination_idx', 'distance']) class OutlierDetector(object): """ A class which contains several outlier detection methods which all return True/False masks as pandas data series, which can be used as masks for the "matches" pandas dataframe which stores match information for each edge of the graph. Attributes ---------- """ def __init__(self): pass # (query only takes care of literal self-matches on a keypoint basis, not self-matches for the whole image) def self_neighbors(self, 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 distance_ratio(self, matches, ratio=0.8): """ Compute and return a mask for the matches dataframe stored on each edge of the graph using the ratio test and distance_ratio set during initialization. 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. ***Will only work as expected if matches already has dropped duplicates*** 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." Returns ------- : dataseries 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. """ #0.8 is Lowe's paper value -- can be changed. mask = [] temp_matches = matches.drop_duplicates() #don't want to deal with duplicates... for key, group in temp_matches.groupby('source_idx'): #won't work if there's only 1 match for each queryIdx if len(group) < 2: mask.append(True) else: if group['distance'].iloc[0] < ratio * group['distance'].iloc[1]: #this means distance _0_ is good and can drop all other distances mask.append(True) for i in range(len(group['distance']-1)): mask.append(False) else: for i in range(len(group['distance'])): mask.append(False) return pd.Series(mask) def mirroring_test(self, matches): """ Compute and return a mask for the matches dataframe on each edge of the graph which will keep only entries in which there is both a source -> destination match and a destination -> source match. 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. Rows are True if the associated keypoint passes the mirroring test and false otherwise. That is, if 1->2, 2->1, both rows will be True, otherwise, they will be false. Keypoints with only one match will be False. Removes duplicate rows. """ return matches.duplicated(keep='first') autocnet/matcher/outlier_detector.py 0 → 100644 +114 −0 Original line number Diff line number Diff line import numpy as np 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 distance_ratio(matches, ratio=0.8): """ Compute and return a mask for a matches dataframe using Lowe's ratio test. 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 = np.zeros(len(matches), dtype=bool) # Pre-allocate the mask counter = 0 for i, group in matches.groupby('source_idx'): group_size = len(group) # If we can not perform the ratio check because all matches are symmetrical if len(group['destination_idx'].unique()) == 1: mask[counter:counter + group_size] = True counter += group_size else: # Otherwise, we can perform the ratio test sorted = group.sort_values(by=['distance']) unique = sorted['distance'].unique() if unique[0] < ratio * unique[1]: mask[counter] = True mask[counter + 1:counter + group_size] = False counter += group_size else: mask[counter: counter + group_size] = False counter += group_size ''' # won't work if there's only 1 match for each queryIdx if len(group) < 2: mask.append(True) else: if group['distance'].iloc[0] < ratio * group['distance'].iloc[1]: # this means distance _0_ is good and can drop all other distances mask.append(True) for i in range(len(group['distance']-1)): mask.append(False) else: for i in range(len(group['distance'])): mask.append(False) ''' return mask def mirroring_test(matches): """ Compute and return a mask for the matches dataframe on each edge of the graph which will keep only entries in which there is both a source -> destination match and a destination -> source match. 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 ------- duplicates : dataseries Intended to mask the matches dataframe. Rows are True if the associated keypoint passes the mirroring test and false otherwise. That is, if 1->2, 2->1, both rows will be True, 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) return duplicates Loading
.travis.yml +1 −1 Original line number Diff line number Diff line Loading @@ -27,7 +27,7 @@ install: - conda info -a # Create a virtual env and install dependencies - conda create -y -q -n test-env python=$TRAVIS_PYTHON_VERSION nose gdal numpy pillow scipy pandas networkx - conda create -y -q -n test-env python=$TRAVIS_PYTHON_VERSION nose gdal numpy pillow scipy pandas networkx scikit-image # Activate the env - source activate test-env Loading
autocnet/fileio/io_controlnetwork.py +0 −2 Original line number Diff line number Diff line Loading @@ -132,7 +132,6 @@ class IsisStore(object): """ point_sizes = [] point_messages = [] print(cnet) for pid, point in cnet.groupby('pid'): # Instantiate the proto spec point_spec = cnf.ControlPointFileEntryV0002() Loading @@ -146,7 +145,6 @@ class IsisStore(object): # A single extend call is cheaper than many add calls to pack points measure_iterable = [] print(point) for name, row in point.iterrows(): measure_spec = point_spec.Measure() measure_spec.serialnumber = row.nid Loading
autocnet/graph/network.py +28 −4 Original line number Diff line number Diff line import operator import os import networkx as nx Loading Loading @@ -223,9 +224,17 @@ class CandidateGraph(nx.Graph): The identifier for the source node destination_key : str The identifier for the destination node outlier_algorithm : object An openCV outlier detections algorithm, e.g. cv2.RANSAC Returns ------- : tuple transformation_matrix : ndarray The 3x3 transformation matrix mask : ndarray Boolean array of the outliers A tuple of the form (transformation matrix, bad entry mask) The returned tuple is empty if there is no edge between the source and destination nodes or if it exists, but has not been populated with a matches dataframe. Loading @@ -250,14 +259,20 @@ class CandidateGraph(nx.Graph): source_keypoints.append(src_keypoint) destination_keypoints.append(dest_keypoint) return cv2.findHomography(np.array(source_keypoints), np.array(destination_keypoints), outlier_algorithm, 5.0) transformation_matrix, mask = cv2.findHomography(np.array(source_keypoints), np.array(destination_keypoints), outlier_algorithm, 5.0) mask = mask.astype(bool) return transformation_matrix, mask else: return ('', '') else: return ('','') def to_cnet(self): def to_cnet(self, clean_keys=[]): """ Generate a control network (C) object from a graph Loading @@ -265,11 +280,20 @@ class CandidateGraph(nx.Graph): ------- merged_cnet : C A control network object clean_keys : list of strings identifying the masking arrays to use, e.g. ratio, symmetry """ merged_cnet = None for source, destination, attributes in self.edges_iter(data=True): matches = attributes['matches'] # Merge all of the masks if clean_keys: mask = np.array(list(map(operator.mul, *[attributes[i] for i in clean_keys]))) matches = matches[mask] kp1 = self.node[source]['keypoints'] kp2 = self.node[destination]['keypoints'] Loading
autocnet/matcher/matcher.py +75 −103 Original line number Diff line number Diff line import cv2 import numpy as np import pandas as pd from autocnet.graph.network import CandidateGraph import numpy as np from skimage.feature import match_template from scipy.misc import imresize FLANN_INDEX_KDTREE = 1 # Algorithm to set centers, DEFAULT_FLANN_PARAMETERS = dict(algorithm=FLANN_INDEX_KDTREE, trees=3) def pattern_match(template, image, upsampling=10, func=match_template): """ Call an arbitrary pattern matcher Parameters ---------- template : ndarray The input search template used to 'query' the destination image image : ndarray The image or sub-image to be searched upsampling : int The multiplier to upsample the template and image. func : object The function to be used to perform the template based matching Returns ------- x : float The x offset y : float The y offset strength : float The strength of the correlation in the range [-1, 1]. """ if upsampling < 1: raise ValueError u_template = imresize(template, (template.shape[0] * upsampling, template.shape[1] * upsampling), interp='bicubic') u_image = imresize(image, (image.shape[0] * upsampling, image.shape[1] * upsampling), interp='bicubic') # Find the the upper left origin of the template in the image match = func(u_image, u_template) y, x = np.unravel_index(np.argmax(match), match.shape) # Resample the match back to the native image resolution x /= upsampling y /= upsampling # Offset from the UL origin to the image center x += (template.shape[1] / 2) y += (template.shape[0] / 2) # Compute the offset to adjust the image match point location ideal_y = image.shape[0] / 2 ideal_x = image.shape[1] / 2 x = ideal_x - x y = ideal_y - y # Find the maximum correlation strength = np.max(match) return x, y, strength class FlannMatcher(object): """ A wrapper to the OpenCV Flann based matcher class that adds Loading Loading @@ -100,104 +173,3 @@ class FlannMatcher(object): return pd.DataFrame(matched, columns=['source_image', 'source_idx', 'destination_image', 'destination_idx', 'distance']) class OutlierDetector(object): """ A class which contains several outlier detection methods which all return True/False masks as pandas data series, which can be used as masks for the "matches" pandas dataframe which stores match information for each edge of the graph. Attributes ---------- """ def __init__(self): pass # (query only takes care of literal self-matches on a keypoint basis, not self-matches for the whole image) def self_neighbors(self, 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 distance_ratio(self, matches, ratio=0.8): """ Compute and return a mask for the matches dataframe stored on each edge of the graph using the ratio test and distance_ratio set during initialization. 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. ***Will only work as expected if matches already has dropped duplicates*** 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." Returns ------- : dataseries 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. """ #0.8 is Lowe's paper value -- can be changed. mask = [] temp_matches = matches.drop_duplicates() #don't want to deal with duplicates... for key, group in temp_matches.groupby('source_idx'): #won't work if there's only 1 match for each queryIdx if len(group) < 2: mask.append(True) else: if group['distance'].iloc[0] < ratio * group['distance'].iloc[1]: #this means distance _0_ is good and can drop all other distances mask.append(True) for i in range(len(group['distance']-1)): mask.append(False) else: for i in range(len(group['distance'])): mask.append(False) return pd.Series(mask) def mirroring_test(self, matches): """ Compute and return a mask for the matches dataframe on each edge of the graph which will keep only entries in which there is both a source -> destination match and a destination -> source match. 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. Rows are True if the associated keypoint passes the mirroring test and false otherwise. That is, if 1->2, 2->1, both rows will be True, otherwise, they will be false. Keypoints with only one match will be False. Removes duplicate rows. """ return matches.duplicated(keep='first')
autocnet/matcher/outlier_detector.py 0 → 100644 +114 −0 Original line number Diff line number Diff line import numpy as np 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 distance_ratio(matches, ratio=0.8): """ Compute and return a mask for a matches dataframe using Lowe's ratio test. 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 = np.zeros(len(matches), dtype=bool) # Pre-allocate the mask counter = 0 for i, group in matches.groupby('source_idx'): group_size = len(group) # If we can not perform the ratio check because all matches are symmetrical if len(group['destination_idx'].unique()) == 1: mask[counter:counter + group_size] = True counter += group_size else: # Otherwise, we can perform the ratio test sorted = group.sort_values(by=['distance']) unique = sorted['distance'].unique() if unique[0] < ratio * unique[1]: mask[counter] = True mask[counter + 1:counter + group_size] = False counter += group_size else: mask[counter: counter + group_size] = False counter += group_size ''' # won't work if there's only 1 match for each queryIdx if len(group) < 2: mask.append(True) else: if group['distance'].iloc[0] < ratio * group['distance'].iloc[1]: # this means distance _0_ is good and can drop all other distances mask.append(True) for i in range(len(group['distance']-1)): mask.append(False) else: for i in range(len(group['distance'])): mask.append(False) ''' return mask def mirroring_test(matches): """ Compute and return a mask for the matches dataframe on each edge of the graph which will keep only entries in which there is both a source -> destination match and a destination -> source match. 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 ------- duplicates : dataseries Intended to mask the matches dataframe. Rows are True if the associated keypoint passes the mirroring test and false otherwise. That is, if 1->2, 2->1, both rows will be True, 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) return duplicates