Loading autocnet/__init__.py +43 −8 Changes for autocnet/__init__.py: 43 added lines, 8 removed lines. Original line number Diff line number Diff line import os import autocnet __version__ = "0.1.0" def get_data(filename): packagdir = autocnet.__path__[0] dirname = os.path.join(os.path.dirname(packagdir), 'data') fullname = os.path.join(dirname, filename) return fullname import autocnet.examples import autocnet.camera import autocnet.cg Loading @@ -18,3 +10,46 @@ import autocnet.matcher import autocnet.transformation import autocnet.utils import autocnet.utils __version__ = "0.1.0" def get_data(filename): packagdir = autocnet.__path__[0] dirname = os.path.join(os.path.dirname(packagdir), 'data') fullname = os.path.join(dirname, filename) return fullname def cuda(enable=False, gpu=0): # Classes/Methods that can vary if GPU is available from autocnet.graph.node import Node from autocnet.graph.edge import Edge if enable: print('Enabling CUDA') try: import cudasift as cs cs.PyInitCuda(gpu) # Here is where the GPU methods get patched into the class from autocnet.matcher.cuda_extractor import extract_features Node._extract_features = staticmethod(extract_features) from autocnet.matcher.cuda_matcher import match Edge.match = match from autocnet.matcher.cuda_decompose import decompose_and_match Edge.decompose_and_match = decompose_and_match except Exception: print('Failed to enable Cuda') return print('CUDA Disabled') # Here is where the CPU methods get patched into the class from autocnet.matcher.feature_extractor import extract_features Node._extract_features = staticmethod(extract_features) from autocnet.matcher.feature_matcher import match Edge.match = match from autocnet.matcher.cpu_decompose import decompose_and_match Edge.decompose_and_match = decompose_and_match cuda() autocnet/graph/edge.py +36 −61 Changes for autocnet/graph/edge.py: 36 added lines, 61 removed lines. Original line number Diff line number Diff line Loading @@ -6,13 +6,12 @@ import pandas as pd from scipy.spatial.distance import cdist import autocnet from autocnet.utils import utils 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.matcher.feature import FlannMatcher from autocnet.transformation.decompose import coupled_decomposition from autocnet.transformation.transformations import FundamentalMatrix, Homography from autocnet.vis.graph_view import plot_edge, plot_node, plot_edge_decomposition from autocnet.cg import cg Loading Loading @@ -74,9 +73,13 @@ class Edge(dict, MutableMapping): # state, dynamically draw the mask from the object. for c in self._masks.columns: if c in mask_lookup: try: truncated_mask = getattr(self, mask_lookup[c]).mask self._masks[c] = False self._masks[c].iloc[truncated_mask.index] = truncated_mask except Exception: #TODO: Get rid of state pass return self._masks @masks.setter Loading Loading @@ -321,8 +324,16 @@ class Edge(dict, MutableMapping): if len(sidx) >= k and len(didx) >=k: mono_matches(self.source, self.destination, sidx, didx) mono_matches(self.destination, self.source, didx, sidx) ======= @property def health(self): return self._health.health def decompose_and_match(*args, **kwargs): pass def match(self, k=2, **kwargs): """ Given two sets of descriptors, utilize a FLANN (Approximate Nearest Neighbor KDTree) matcher to find the k nearest matches. Nearness is Loading @@ -335,65 +346,7 @@ class Edge(dict, MutableMapping): k : int The number of neighbors to find """ def mono_matches(a, b, aidx=None, bidx=None): """ Apply the FLANN match_features Parameters ---------- a : object A node object b : object A node object aidx : iterable An index for the descriptors to subset bidx : iterable An index for the descriptors to subset """ # Subset if requested if aidx is not None: ad = a.descriptors[aidx] else: ad = a.descriptors if bidx is not None: bd = b.descriptors[bidx] else: bd = b.descriptors # Load, train, and match fl.add(ad, a['node_id'], index=aidx) fl.train() matches = fl.query(bd, b['node_id'], k, index=bidx) self._add_matches(matches) fl.clear() fl = FlannMatcher() mono_matches(self.source, self.destination) mono_matches(self.destination, self.source) def _add_matches(self, matches): """ Given a dataframe of matches, either append to an existing matches edge attribute or initially populate said attribute. Parameters ---------- matches : dataframe A dataframe of matches """ if self.matches is None: self.matches = matches else: df = self.matches self.matches = df.append(matches, ignore_index=True, verify_integrity=True) pass def symmetry_check(self): if hasattr(self, 'matches'): Loading Loading @@ -767,3 +720,25 @@ class Edge(dict, MutableMapping): raise AttributeError('Matches have not been computed for this edge') voronoi = cg.vor(self, clean_keys, **kwargs) self.matches = pd.concat([self.matches, voronoi[1]['vor_weights']], axis=1) def decompose(self, maxiterations=3): """ Apply coupled decomposition to the images and match identified sub-images Parameters ---------- maxiterations : int The number of iterations. Appropriate values: | Number of megapixels | k | |----------------------|---| | m < 10 |1-2| | 10 < m < 30 | 3 | | 30 < m < 100 | 4 | | 100 < m < 1000 | 5 | | m > 1000 | 6 | """ pass autocnet/graph/network.py +5 −5 Changes for autocnet/graph/network.py: 5 added lines, 5 removed lines. Original line number Diff line number Diff line Loading @@ -205,7 +205,7 @@ class CandidateGraph(nx.Graph): raise NotImplementedError def extract_features(self, method='orb', extractor_parameters={}): def extract_features(self, *args, **kwargs): """ Extracts features from each image in the graph and uses the result to assign the node attributes for 'handle', 'image', 'keypoints', and 'descriptors'. Loading @@ -223,8 +223,7 @@ class CandidateGraph(nx.Graph): """ for i, node in self.nodes_iter(data=True): image = node.get_array() node.extract_features(image, method=method, extractor_parameters=extractor_parameters) node.extract_features(image, *args, **kwargs), def save_features(self, out_path, nodes=[]): """ Loading Loading @@ -284,7 +283,7 @@ class CandidateGraph(nx.Graph): hdf = None def match_features(self, *args, **kwargs): def match(self, *args, **kwargs): """ For all connected edges in the graph, apply feature matching Loading @@ -294,7 +293,8 @@ class CandidateGraph(nx.Graph): """ self.apply_func_to_edges('match', *args, **kwargs) def decompose_and_match_features(self, *args, **kwargs): def decompose_and_match(self, *args, **kwargs): """ For all edges in the graph, apply coupled decomposition followed by feature matching. Loading autocnet/graph/node.py +6 −2 Changes for autocnet/graph/node.py: 6 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -261,7 +261,8 @@ class Node(dict, MutableMapping): return keypoints def extract_features(self, array, **kwargs): @staticmethod def _extract_features(*args, **kwargs): """ Extract features for the node Loading @@ -273,7 +274,10 @@ class Node(dict, MutableMapping): kwargs passed to autocnet.feature_extractor.extract_features """ self._keypoints, self._descriptors = fe.extract_features(array, **kwargs) pass def extract_features(self, *args, **kwargs): self._keypoints, self.descriptors = Node._extract_features(*args, **kwargs) def load_features(self, in_path): """ Loading autocnet/matcher/cpu_decompose.py 0 → 100644 +249 −0 Changes for autocnet/matcher/cpu_decompose.py: 249 added lines, 0 removed lines. Original line number Diff line number Diff line import numpy as np from scipy.spatial.distance import cdist from autocnet.matcher.feature import FlannMatcher from autocnet.transformation.decompose import coupled_decomposition def decompose_and_match(self, k=2, maxiteration=3, size=18, buf_dist=3,**kwargs): """ Similar to match, this method first decomposed the image into $4^{maxiteration}$ subimages and applys matching between each sub-image. This method is potential slower than the standard match due to the overhead in matching, but can be significantly more accurate. The increase in accuracy is a function of the total image size. Suggested values for maxiteration are provided below. Parameters ---------- k : int The number of neighbors to find method : {'coupled', 'whole'} whether to utilize coupled decomposition or match the whole image maxiteration : int When using coupled decomposition, the number of recursive divisions to apply. The total number of resultant sub-images will be 4 ** maxiteration. Approximate values: | Number of megapixels | maxiteration | |----------------------|--------------| | m < 10 |1-2| | 10 < m < 30 | 3 | | 30 < m < 100 | 4 | | 100 < m < 1000 | 5 | | m > 1000 | 6 | size : int When using coupled decomposition, the total number of points to check in each sub-image to try and find a match. Selection of this number is a balance between seeking a representative mid-point and computational cost. buf_dist : int When using coupled decomposition, the distance from the edge of the (sub)image a point must be in order to be used as a partioning point. The smaller the distance, the more likely percision errors can results in erroneous partitions. """ def mono_matches(a, b, aidx=None, bidx=None): """ Apply the FLANN match_features Parameters ---------- a : object A node object b : object A node object aidx : iterable An index for the descriptors to subset bidx : iterable An index for the descriptors to subset """ # Subset if requested if aidx is not None: ad = a.descriptors[aidx] else: ad = a.descriptors if bidx is not None: bd = b.descriptors[bidx] else: bd = b.descriptors # Load, train, and match fl.add(ad, a.node_id, index=aidx) fl.train() matches = fl.query(bd, b.node_id, k, index=bidx) if self.matches is None: self.matches = matches else: df = self.matches self.matches = df.append(matches, ignore_index=True, verify_integrity=True) fl.clear() def func(group): ratio = 0.8 res = [False] * len(group) if len(res) == 1: return [single] if group.iloc[0] < group.iloc[1] * ratio: res[0] = True return res # Grab the original image arrays sdata = self.source.get_array() ddata = self.destination.get_array() ssize = sdata.shape dsize = ddata.shape # Grab all the available candidate keypoints skp = self.source.get_keypoints() dkp = self.destination.get_keypoints() # Set up the membership arrays self.smembership = np.zeros(sdata.shape, dtype=np.int16) self.dmembership = np.zeros(ddata.shape, dtype=np.int16) self.smembership[:] = -1 self.dmembership[:] = -1 pcounter = 0 # FLANN Matcher fl= FlannMatcher() for k in range(maxiteration): partitions = np.unique(self.smembership) for p in partitions: sy_part, sx_part = np.where(self.smembership == p) dy_part, dx_part = np.where(self.dmembership == p) # Get the source extent minsy = np.min(sy_part) maxsy = np.max(sy_part) + 1 minsx = np.min(sx_part) maxsx = np.max(sx_part) + 1 # Get the destination extent mindy = np.min(dy_part) maxdy = np.max(dy_part) + 1 mindx = np.min(dx_part) maxdx = np.max(dx_part) + 1 # Clip the sub image from the full images asub = sdata[minsy:maxsy, minsx:maxsx] bsub = ddata[mindy:maxdy, mindx:maxdx] # Utilize the FLANN matcher to find a match to approximate a center fl.add(self.destination.descriptors, self.destination.node_id) fl.train() scounter = 0 decompose = False while True: sub_skp = skp.query('x >= {} and x <= {} and y >= {} and y <= {}'.format(minsx, maxsx, minsy, maxsy)) # Check the size to ensure a valid return if len(sub_skp) == 0: break # No valid keypoints in this (sub)image if size > len(sub_skp): size = len(sub_skp) candidate_idx = np.random.choice(sub_skp.index, size=size, replace=False) candidates = self.source.descriptors[candidate_idx] matches = fl.query(candidates, self.source.node_id, k=3, index=candidate_idx) # Apply Lowe's ratio test to try to find a 'good' starting point mask = matches.groupby('source_idx')['distance'].transform(func).astype('bool') candidate_matches = matches[mask] match_idx = candidate_matches['source_idx'] # Extract those matches that pass the ratio check sub_skp = skp.iloc[match_idx] # Check that valid points remain if len(sub_skp) == 0: break # Locate the candidate closest to the middle of all of the matches smx, smy = sub_skp[['x', 'y']].mean() mid = np.array([[smx, smy]]) dists = cdist(mid, sub_skp[['x', 'y']]) closest = sub_skp.iloc[np.argmin(dists)] closest_idx = closest.name soriginx, soriginy = closest[['x', 'y']] # Grab the corresponding point in the destination q = candidate_matches.query('source_idx == {}'.format(closest.name)) dest_idx = q['destination_idx'].iat[0] doriginx = dkp.at[dest_idx, 'x'] doriginy = dkp.at[dest_idx, 'y'] if mindy + buf_dist <= doriginy <= maxdy - buf_dist\ and mindx + 3 <= doriginx <= maxdx - 3: # Point is good to split on decompose = True break else: scounter += 1 if scounter >= maxiteration: break # Clear the Flann matcher for reuse fl.clear() # Check that the identified match falls within the (sub)image # This catches most bad matches that have passed the ratio check if not (buf_dist <= doriginx - mindx <= bsub.shape[1] - buf_dist) or not\ (buf_dist <= doriginy - mindy <= bsub.shape[0] - buf_dist): decompose = False if decompose: # Apply coupled decomposition, shifting the origin to the sub-image s_submembership, d_submembership = coupled_decomposition(asub, bsub, sorigin=(soriginx - minsx, soriginy - minsy), dorigin=(doriginx - mindx, doriginy - mindy), **kwargs) # Shift the returned membership counters to a set of unique numbers s_submembership += pcounter d_submembership += pcounter # And assign membership self.smembership[minsy:maxsy, minsx:maxsx] = s_submembership self.dmembership[mindy:maxdy, mindx:maxdx] = d_submembership pcounter += 4 # Now match the decomposed segments to one another for p in np.unique(self.smembership): sy_part, sx_part = np.where(self.smembership == p) dy_part, dx_part = np.where(self.dmembership == p) # Get the source extent minsy = np.min(sy_part) maxsy = np.max(sy_part) + 1 minsx = np.min(sx_part) maxsx = np.max(sx_part) + 1 # Get the destination extent mindy = np.min(dy_part) maxdy = np.max(dy_part) + 1 mindx = np.min(dx_part) maxdx = np.max(dx_part) + 1 # Get the indices of the candidate keypoints within those regions / variables are pulled before decomp. sidx = skp.query('x >= {} and x <= {} and y >= {} and y <= {}'.format(minsx, maxsx, minsy, maxsy)).index didx = dkp.query('x >= {} and x <= {} and y >= {} and y <= {}'.format(mindx, maxdx, mindy, maxdy)).index # If the candidates < k, OpenCV throws an error if len(sidx) >= k and len(didx) >=k: mono_matches(self.source, self.destination, sidx, didx) mono_matches(self.destination, self.source, didx, sidx) Loading
autocnet/__init__.py +43 −8 Changes for autocnet/__init__.py: 43 added lines, 8 removed lines. Original line number Diff line number Diff line import os import autocnet __version__ = "0.1.0" def get_data(filename): packagdir = autocnet.__path__[0] dirname = os.path.join(os.path.dirname(packagdir), 'data') fullname = os.path.join(dirname, filename) return fullname import autocnet.examples import autocnet.camera import autocnet.cg Loading @@ -18,3 +10,46 @@ import autocnet.matcher import autocnet.transformation import autocnet.utils import autocnet.utils __version__ = "0.1.0" def get_data(filename): packagdir = autocnet.__path__[0] dirname = os.path.join(os.path.dirname(packagdir), 'data') fullname = os.path.join(dirname, filename) return fullname def cuda(enable=False, gpu=0): # Classes/Methods that can vary if GPU is available from autocnet.graph.node import Node from autocnet.graph.edge import Edge if enable: print('Enabling CUDA') try: import cudasift as cs cs.PyInitCuda(gpu) # Here is where the GPU methods get patched into the class from autocnet.matcher.cuda_extractor import extract_features Node._extract_features = staticmethod(extract_features) from autocnet.matcher.cuda_matcher import match Edge.match = match from autocnet.matcher.cuda_decompose import decompose_and_match Edge.decompose_and_match = decompose_and_match except Exception: print('Failed to enable Cuda') return print('CUDA Disabled') # Here is where the CPU methods get patched into the class from autocnet.matcher.feature_extractor import extract_features Node._extract_features = staticmethod(extract_features) from autocnet.matcher.feature_matcher import match Edge.match = match from autocnet.matcher.cpu_decompose import decompose_and_match Edge.decompose_and_match = decompose_and_match cuda()
autocnet/graph/edge.py +36 −61 Changes for autocnet/graph/edge.py: 36 added lines, 61 removed lines. Original line number Diff line number Diff line Loading @@ -6,13 +6,12 @@ import pandas as pd from scipy.spatial.distance import cdist import autocnet from autocnet.utils import utils 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.matcher.feature import FlannMatcher from autocnet.transformation.decompose import coupled_decomposition from autocnet.transformation.transformations import FundamentalMatrix, Homography from autocnet.vis.graph_view import plot_edge, plot_node, plot_edge_decomposition from autocnet.cg import cg Loading Loading @@ -74,9 +73,13 @@ class Edge(dict, MutableMapping): # state, dynamically draw the mask from the object. for c in self._masks.columns: if c in mask_lookup: try: truncated_mask = getattr(self, mask_lookup[c]).mask self._masks[c] = False self._masks[c].iloc[truncated_mask.index] = truncated_mask except Exception: #TODO: Get rid of state pass return self._masks @masks.setter Loading Loading @@ -321,8 +324,16 @@ class Edge(dict, MutableMapping): if len(sidx) >= k and len(didx) >=k: mono_matches(self.source, self.destination, sidx, didx) mono_matches(self.destination, self.source, didx, sidx) ======= @property def health(self): return self._health.health def decompose_and_match(*args, **kwargs): pass def match(self, k=2, **kwargs): """ Given two sets of descriptors, utilize a FLANN (Approximate Nearest Neighbor KDTree) matcher to find the k nearest matches. Nearness is Loading @@ -335,65 +346,7 @@ class Edge(dict, MutableMapping): k : int The number of neighbors to find """ def mono_matches(a, b, aidx=None, bidx=None): """ Apply the FLANN match_features Parameters ---------- a : object A node object b : object A node object aidx : iterable An index for the descriptors to subset bidx : iterable An index for the descriptors to subset """ # Subset if requested if aidx is not None: ad = a.descriptors[aidx] else: ad = a.descriptors if bidx is not None: bd = b.descriptors[bidx] else: bd = b.descriptors # Load, train, and match fl.add(ad, a['node_id'], index=aidx) fl.train() matches = fl.query(bd, b['node_id'], k, index=bidx) self._add_matches(matches) fl.clear() fl = FlannMatcher() mono_matches(self.source, self.destination) mono_matches(self.destination, self.source) def _add_matches(self, matches): """ Given a dataframe of matches, either append to an existing matches edge attribute or initially populate said attribute. Parameters ---------- matches : dataframe A dataframe of matches """ if self.matches is None: self.matches = matches else: df = self.matches self.matches = df.append(matches, ignore_index=True, verify_integrity=True) pass def symmetry_check(self): if hasattr(self, 'matches'): Loading Loading @@ -767,3 +720,25 @@ class Edge(dict, MutableMapping): raise AttributeError('Matches have not been computed for this edge') voronoi = cg.vor(self, clean_keys, **kwargs) self.matches = pd.concat([self.matches, voronoi[1]['vor_weights']], axis=1) def decompose(self, maxiterations=3): """ Apply coupled decomposition to the images and match identified sub-images Parameters ---------- maxiterations : int The number of iterations. Appropriate values: | Number of megapixels | k | |----------------------|---| | m < 10 |1-2| | 10 < m < 30 | 3 | | 30 < m < 100 | 4 | | 100 < m < 1000 | 5 | | m > 1000 | 6 | """ pass
autocnet/graph/network.py +5 −5 Changes for autocnet/graph/network.py: 5 added lines, 5 removed lines. Original line number Diff line number Diff line Loading @@ -205,7 +205,7 @@ class CandidateGraph(nx.Graph): raise NotImplementedError def extract_features(self, method='orb', extractor_parameters={}): def extract_features(self, *args, **kwargs): """ Extracts features from each image in the graph and uses the result to assign the node attributes for 'handle', 'image', 'keypoints', and 'descriptors'. Loading @@ -223,8 +223,7 @@ class CandidateGraph(nx.Graph): """ for i, node in self.nodes_iter(data=True): image = node.get_array() node.extract_features(image, method=method, extractor_parameters=extractor_parameters) node.extract_features(image, *args, **kwargs), def save_features(self, out_path, nodes=[]): """ Loading Loading @@ -284,7 +283,7 @@ class CandidateGraph(nx.Graph): hdf = None def match_features(self, *args, **kwargs): def match(self, *args, **kwargs): """ For all connected edges in the graph, apply feature matching Loading @@ -294,7 +293,8 @@ class CandidateGraph(nx.Graph): """ self.apply_func_to_edges('match', *args, **kwargs) def decompose_and_match_features(self, *args, **kwargs): def decompose_and_match(self, *args, **kwargs): """ For all edges in the graph, apply coupled decomposition followed by feature matching. Loading
autocnet/graph/node.py +6 −2 Changes for autocnet/graph/node.py: 6 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -261,7 +261,8 @@ class Node(dict, MutableMapping): return keypoints def extract_features(self, array, **kwargs): @staticmethod def _extract_features(*args, **kwargs): """ Extract features for the node Loading @@ -273,7 +274,10 @@ class Node(dict, MutableMapping): kwargs passed to autocnet.feature_extractor.extract_features """ self._keypoints, self._descriptors = fe.extract_features(array, **kwargs) pass def extract_features(self, *args, **kwargs): self._keypoints, self.descriptors = Node._extract_features(*args, **kwargs) def load_features(self, in_path): """ Loading
autocnet/matcher/cpu_decompose.py 0 → 100644 +249 −0 Changes for autocnet/matcher/cpu_decompose.py: 249 added lines, 0 removed lines. Original line number Diff line number Diff line import numpy as np from scipy.spatial.distance import cdist from autocnet.matcher.feature import FlannMatcher from autocnet.transformation.decompose import coupled_decomposition def decompose_and_match(self, k=2, maxiteration=3, size=18, buf_dist=3,**kwargs): """ Similar to match, this method first decomposed the image into $4^{maxiteration}$ subimages and applys matching between each sub-image. This method is potential slower than the standard match due to the overhead in matching, but can be significantly more accurate. The increase in accuracy is a function of the total image size. Suggested values for maxiteration are provided below. Parameters ---------- k : int The number of neighbors to find method : {'coupled', 'whole'} whether to utilize coupled decomposition or match the whole image maxiteration : int When using coupled decomposition, the number of recursive divisions to apply. The total number of resultant sub-images will be 4 ** maxiteration. Approximate values: | Number of megapixels | maxiteration | |----------------------|--------------| | m < 10 |1-2| | 10 < m < 30 | 3 | | 30 < m < 100 | 4 | | 100 < m < 1000 | 5 | | m > 1000 | 6 | size : int When using coupled decomposition, the total number of points to check in each sub-image to try and find a match. Selection of this number is a balance between seeking a representative mid-point and computational cost. buf_dist : int When using coupled decomposition, the distance from the edge of the (sub)image a point must be in order to be used as a partioning point. The smaller the distance, the more likely percision errors can results in erroneous partitions. """ def mono_matches(a, b, aidx=None, bidx=None): """ Apply the FLANN match_features Parameters ---------- a : object A node object b : object A node object aidx : iterable An index for the descriptors to subset bidx : iterable An index for the descriptors to subset """ # Subset if requested if aidx is not None: ad = a.descriptors[aidx] else: ad = a.descriptors if bidx is not None: bd = b.descriptors[bidx] else: bd = b.descriptors # Load, train, and match fl.add(ad, a.node_id, index=aidx) fl.train() matches = fl.query(bd, b.node_id, k, index=bidx) if self.matches is None: self.matches = matches else: df = self.matches self.matches = df.append(matches, ignore_index=True, verify_integrity=True) fl.clear() def func(group): ratio = 0.8 res = [False] * len(group) if len(res) == 1: return [single] if group.iloc[0] < group.iloc[1] * ratio: res[0] = True return res # Grab the original image arrays sdata = self.source.get_array() ddata = self.destination.get_array() ssize = sdata.shape dsize = ddata.shape # Grab all the available candidate keypoints skp = self.source.get_keypoints() dkp = self.destination.get_keypoints() # Set up the membership arrays self.smembership = np.zeros(sdata.shape, dtype=np.int16) self.dmembership = np.zeros(ddata.shape, dtype=np.int16) self.smembership[:] = -1 self.dmembership[:] = -1 pcounter = 0 # FLANN Matcher fl= FlannMatcher() for k in range(maxiteration): partitions = np.unique(self.smembership) for p in partitions: sy_part, sx_part = np.where(self.smembership == p) dy_part, dx_part = np.where(self.dmembership == p) # Get the source extent minsy = np.min(sy_part) maxsy = np.max(sy_part) + 1 minsx = np.min(sx_part) maxsx = np.max(sx_part) + 1 # Get the destination extent mindy = np.min(dy_part) maxdy = np.max(dy_part) + 1 mindx = np.min(dx_part) maxdx = np.max(dx_part) + 1 # Clip the sub image from the full images asub = sdata[minsy:maxsy, minsx:maxsx] bsub = ddata[mindy:maxdy, mindx:maxdx] # Utilize the FLANN matcher to find a match to approximate a center fl.add(self.destination.descriptors, self.destination.node_id) fl.train() scounter = 0 decompose = False while True: sub_skp = skp.query('x >= {} and x <= {} and y >= {} and y <= {}'.format(minsx, maxsx, minsy, maxsy)) # Check the size to ensure a valid return if len(sub_skp) == 0: break # No valid keypoints in this (sub)image if size > len(sub_skp): size = len(sub_skp) candidate_idx = np.random.choice(sub_skp.index, size=size, replace=False) candidates = self.source.descriptors[candidate_idx] matches = fl.query(candidates, self.source.node_id, k=3, index=candidate_idx) # Apply Lowe's ratio test to try to find a 'good' starting point mask = matches.groupby('source_idx')['distance'].transform(func).astype('bool') candidate_matches = matches[mask] match_idx = candidate_matches['source_idx'] # Extract those matches that pass the ratio check sub_skp = skp.iloc[match_idx] # Check that valid points remain if len(sub_skp) == 0: break # Locate the candidate closest to the middle of all of the matches smx, smy = sub_skp[['x', 'y']].mean() mid = np.array([[smx, smy]]) dists = cdist(mid, sub_skp[['x', 'y']]) closest = sub_skp.iloc[np.argmin(dists)] closest_idx = closest.name soriginx, soriginy = closest[['x', 'y']] # Grab the corresponding point in the destination q = candidate_matches.query('source_idx == {}'.format(closest.name)) dest_idx = q['destination_idx'].iat[0] doriginx = dkp.at[dest_idx, 'x'] doriginy = dkp.at[dest_idx, 'y'] if mindy + buf_dist <= doriginy <= maxdy - buf_dist\ and mindx + 3 <= doriginx <= maxdx - 3: # Point is good to split on decompose = True break else: scounter += 1 if scounter >= maxiteration: break # Clear the Flann matcher for reuse fl.clear() # Check that the identified match falls within the (sub)image # This catches most bad matches that have passed the ratio check if not (buf_dist <= doriginx - mindx <= bsub.shape[1] - buf_dist) or not\ (buf_dist <= doriginy - mindy <= bsub.shape[0] - buf_dist): decompose = False if decompose: # Apply coupled decomposition, shifting the origin to the sub-image s_submembership, d_submembership = coupled_decomposition(asub, bsub, sorigin=(soriginx - minsx, soriginy - minsy), dorigin=(doriginx - mindx, doriginy - mindy), **kwargs) # Shift the returned membership counters to a set of unique numbers s_submembership += pcounter d_submembership += pcounter # And assign membership self.smembership[minsy:maxsy, minsx:maxsx] = s_submembership self.dmembership[mindy:maxdy, mindx:maxdx] = d_submembership pcounter += 4 # Now match the decomposed segments to one another for p in np.unique(self.smembership): sy_part, sx_part = np.where(self.smembership == p) dy_part, dx_part = np.where(self.dmembership == p) # Get the source extent minsy = np.min(sy_part) maxsy = np.max(sy_part) + 1 minsx = np.min(sx_part) maxsx = np.max(sx_part) + 1 # Get the destination extent mindy = np.min(dy_part) maxdy = np.max(dy_part) + 1 mindx = np.min(dx_part) maxdx = np.max(dx_part) + 1 # Get the indices of the candidate keypoints within those regions / variables are pulled before decomp. sidx = skp.query('x >= {} and x <= {} and y >= {} and y <= {}'.format(minsx, maxsx, minsy, maxsy)).index didx = dkp.query('x >= {} and x <= {} and y >= {} and y <= {}'.format(mindx, maxdx, mindy, maxdy)).index # If the candidates < k, OpenCV throws an error if len(sidx) >= k and len(didx) >=k: mono_matches(self.source, self.destination, sidx, didx) mono_matches(self.destination, self.source, didx, sidx)