Loading autocnet/graph/network.py +70 −16 Changes for autocnet/graph/network.py: 70 added lines, 16 removed lines. Original line number Diff line number Diff line Loading @@ -15,6 +15,12 @@ from autocnet.graph.node import Node from autocnet.io import network as io_network from autocnet.vis.graph_view import plot_graph, cluster_plot # The total number of pixels squared that can fit into the keys number of GB of RAM for SIFT. MAXSIZE = {0:None, 2:6250, 4:8840, 8:12500, 12:15310} class CandidateGraph(nx.Graph): """ Loading Loading @@ -80,6 +86,20 @@ class CandidateGraph(nx.Graph): eq = False return eq @property def maxsize(self): if not hasattr(self, '_maxsize'): self._maxsize = MAXSIZE[0] return self._maxsize @maxsize.setter def maxsize(self, value): if not value in MAXSIZE.keys(): raise KeyError('Value must be in {}'.format(','.join(map(str,MAXSIZE.keys())))) else: self._maxsize = MAXSIZE[value] @classmethod def from_filelist(cls, filelist, basepath=None): """ Loading Loading @@ -197,25 +217,49 @@ class CandidateGraph(nx.Graph): raise NotImplementedError def extract_features(self, *args, **kwargs): def extract_features(self, band=1, *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'. """ for i, node in self.nodes_iter(data=True): array = node.geodata.read_array(band=band) node.extract_features(array, *args, **kwargs), def extract_features_with_downsampling(self, downsample_amount=None, *args, **kwargs): """ Extract interest points from a downsampled array. The array is downsampled by the downsample_amount keyword using the Lanconz downsample amount. If the downsample keyword is not supplied, compute a downsampling constant as the total array size divided by the network maxsize attribute. Parameters ---------- method : {'orb', 'sift', 'fast'} The descriptor method to be used extractor_parameters : dict A dictionary containing OpenCV SIFT parameters names and values. downsampling : int The divisor to image_size to down sample the input image. downsample_amount : int The amount of downsampling to apply to the image """ for i, node in self.nodes_iter(data=True): image = node.get_array() node.extract_features(image, *args, **kwargs), if downsample_amount == None: total_size = node.geodata.raster_size[0] * node.geodata.raster_size[1] downsample_amount = math.ceil(total_size / self.maxsize**2) node.extract_features_with_downsampling(downsample_amount, *args, **kwargs) def extract_features_with_tiling(self, tilesize=1000, overlap=500, *args, **kwargs): for i, node in self.nodes_iter(data=True): node.extract_features_with_tiling(tilesize=tilesize, overlap=overlap, *args, **kwargs) def extract_subsets(self, *args, **kwargs): """ Extracts features from each image in those regions estimated to be overlapping. *args and **kwargs are passed to the feature extractor. For example, passing method='sift' will cause the extractor to use the sift method. """ for source, destination, e in self.edges_iter(data=True): e.extract_subset(*args, **kwargs) def save_features(self, out_path, nodes=[], **kwargs): """ Loading Loading @@ -279,6 +323,17 @@ class CandidateGraph(nx.Graph): """ self.apply_func_to_edges('decompose_and_match', *args, **kwargs) def estimate_mbrs(self, *args, **kwargs): """ For each edge, estimate the overlap and compute a minimum bounding rectangle (mbr) in pixel space. See Also -------- autocnet.graoh.edge.Edge.compute_mbr """ self.apply_func_to_edges('estimate_mbr', *args, **kwargs) def compute_clusters(self, func=markov_cluster.mcl, *args, **kwargs): """ Apply some graph clustering algorithm to compute a subset of the global Loading Loading @@ -374,7 +429,7 @@ class CandidateGraph(nx.Graph): See Also -------- autocnet.matcher.outlier_detector.DistanceRatio.compute autocnet.matcher.cpu_outlier_detector.DistanceRatio.compute ''' self.apply_func_to_edges('ratio_check', *args, **kwargs) Loading @@ -385,7 +440,7 @@ class CandidateGraph(nx.Graph): See Also -------- autocnet.graph.edge.Edge.compute_homography autocnet.matcher.outlier_detector.compute_homography autocnet.matcher.cpu_outlier_detector.compute_homography ''' self.apply_func_to_edges('compute_homography', *args, **kwargs) Loading @@ -395,7 +450,7 @@ class CandidateGraph(nx.Graph): See Also -------- autocnet.matcher.outlier_detector.compute_fundamental_matrix autocnet.matcher.cpu_outlier_detector.compute_fundamental_matrix ''' self.apply_func_to_edges('compute_fundamental_matrix', *args, **kwargs) Loading @@ -415,7 +470,7 @@ class CandidateGraph(nx.Graph): See Also -------- autocnet.matcher.outlier_detector.SpatialSuppression autocnet.matcher.cpu_outlier_detector.SpatialSuppression ''' self.apply_func_to_edges('suppress', *args, **kwargs) Loading Loading @@ -644,8 +699,7 @@ class CandidateGraph(nx.Graph): # get all edges that have matches matches = [(u, v) for u, v, edge in self.edges_iter(data=True) if hasattr(edge, 'matches') and not edge.matches is None] if not edge.matches.empty] return self.create_edge_subgraph(matches) Loading autocnet/graph/node.py +123 −58 Changes for autocnet/graph/node.py: 123 added lines, 58 removed lines. Original line number Diff line number Diff line from collections import defaultdict, MutableMapping import itertools import os import warnings Loading @@ -6,7 +7,7 @@ import numpy as np import pandas as pd from plio.io.io_gdal import GeoDataset from plio.io.isis_serial_number import generate_serial_number from scipy.misc import bytescale from scipy.misc import bytescale, imresize from autocnet.cg import cg from autocnet.control.control import Correspondence, Point Loading @@ -14,8 +15,8 @@ from autocnet.control.control import Correspondence, Point from autocnet.io import keypoints as io_keypoints from autocnet.matcher.add_depth import deepen_correspondences from autocnet.matcher import feature_extractor as fe from autocnet.matcher import outlier_detector as od from autocnet.matcher import cpu_extractor as fe from autocnet.matcher import cpu_outlier_detector as od from autocnet.matcher import suppression_funcs as spf from autocnet.cg.cg import convex_hull_ratio Loading Loading @@ -67,6 +68,8 @@ class Node(dict, MutableMapping): self.point_to_correspondence = defaultdict(set) self.point_to_correspondence_df = None self.descriptors = None self.keypoints = pd.DataFrame() self.masks = pd.DataFrame() def __repr__(self): return """ Loading @@ -87,11 +90,9 @@ class Node(dict, MutableMapping): if isinstance(v, pd.DataFrame): if not v.equals(o[k]): eq = False print('N', k) elif isinstance(v, np.ndarray): if not v.all() == o[k].all(): eq = False print('N2', k) return eq """ def __getitem__(self, item): Loading Loading @@ -119,16 +120,16 @@ class Node(dict, MutableMapping): else: return None @property """ @property def masks(self): mask_lookup = {'suppression': 'suppression'} if not hasattr(self, '_keypoints'): if self.keypoints is None: warnings.warn('Keypoints have not been extracted') return if not hasattr(self, '_masks'): self._masks = pd.DataFrame(index=self._keypoints.index) self._masks = pd.DataFrame(index=self.keypoints.index) # If the mask is coming form another object that tracks # state, dynamically draw the mask from the object. Loading @@ -142,7 +143,7 @@ class Node(dict, MutableMapping): column_name = v[0] boolean_mask = v[1] self.masks[column_name] = boolean_mask """ @property def isis_serial(self): """ Loading @@ -159,24 +160,7 @@ class Node(dict, MutableMapping): @property def nkeypoints(self): if hasattr(self, '_keypoints'): return len(self._keypoints) else: return 0 """ @property def keypoints(self): if hasattr(self, '_keypoints'): return self._keypoints.copy() else: return None @property def descriptors(self): if hasattr(self, '_descriptors'): return np.copy(self._descriptors) else: return None""" return len(self.keypoints) def coverage(self): """ Loading Loading @@ -235,23 +219,19 @@ class Node(dict, MutableMapping): """ Return the keypoints for the node. If index is passed, return the appropriate subset. Parameters ---------- index : iterable indices for of the keypoints to return Returns ------- : dataframe A pandas dataframe of keypoints """ if hasattr(self, '_keypoints'): if index is not None: return self._keypoints.loc[index] return self.keypoints.loc[index] else: return self._keypoints return self.keypoints def get_keypoint_coordinates(self, index=None, homogeneous=False): """ Loading @@ -271,7 +251,10 @@ class Node(dict, MutableMapping): : dataframe A pandas dataframe of keypoint coordinates """ keypoints = self.get_keypoints(index=index)[['x', 'y']] if index is None: keypoints = self.keypoints[['x', 'y']] else: keypoints = self.keypoints.loc[index][['x', 'y']] if homogeneous: keypoints['homogeneous'] = 1 Loading @@ -279,7 +262,7 @@ class Node(dict, MutableMapping): return keypoints @staticmethod def _extract_features(*args, **kwargs): def _extract_features(array, *args, **kwargs): """ Extract features for the node Loading @@ -288,13 +271,100 @@ class Node(dict, MutableMapping): array : ndarray kwargs : dict kwargs passed to autocnet.feature_extractor.extract_features kwargs passed to autocnet.cpu_extractor.extract_features """ pass def extract_features(self, *args, **kwargs): self._keypoints, self.descriptors = Node._extract_features(*args, **kwargs) def extract_features(self, array, xystart=[], *args, **kwargs): arraysize = array.shape[0] * array.shape[1] try: maxsize = self.maxsize[0] * self.maxsize[1] except: maxsize = np.inf if arraysize > maxsize: warnings.warn('Node: {}. Maximum feature extraction array size is {}. Maximum array size is {}. Please use tiling or downsampling.'.format(self['node_id'], maxsize, arraysize)) keypoints, descriptors = Node._extract_features(array, *args, **kwargs) count = len(self.keypoints) if xystart: keypoints['x'] += xystart[0] keypoints['y'] += xystart[1] self.keypoints = pd.concat((self.keypoints, keypoints)) descriptor_mask = self.keypoints.duplicated()[count:] number_new = descriptor_mask.sum() # Removed duplicated and re-index the merged keypoints self.keypoints.drop_duplicates(inplace=True) self.keypoints.reset_index(inplace=True, drop=True) if self.descriptors is not None: self.descriptors = np.concatenate((self.descriptors, descriptors[~descriptor_mask])) else: self.descriptors = descriptors def extract_features_from_overlaps(self, overlaps=[], downsampling=False, tiling=False, *args, **kwargs): # iterate through the overlaps # check for downsampling or tiling and dispatch as needed to that func # that should then dispatch to the extract features func pass def extract_features_with_downsampling(self, downsample_amount, array_read_args={}, interp='lanczos', *args, **kwargs): """ Extract interest points for the this node (image) by first downsampling, then applying the extractor, and then upsampling the results backin to true image space. Parameters ---------- downsample_amount : int The amount to downsample by """ array_size = self.geodata.raster_size total_size = array_size[0] * array_size[1] shape = (int(array_size[0] / downsample_amount), int(array_size[1] / downsample_amount)) array = imresize(self.geodata.read_array(**array_read_args), shape, interp=interp) self.extract_features(array, *args, **kwargs) self.keypoints['x'] *= downsample_amount self.keypoints['y'] *= downsample_amount def extract_features_with_tiling(self, tilesize=1000, overlap=500, *args, **kwargs): array_size = self.geodata.raster_size stepsize = tilesize - overlap if stepsize < 0: raise ValueError('Overlap can not be greater than tilesize.') # Compute the tiles ystarts = range(0, array_size[1], stepsize) ystops = range(tilesize, array_size[1], stepsize) ytiles = list(zip(ystarts, ystops)) ytiles.append((ytiles[-1][0] + stepsize, array_size[1])) xstarts = range(0, array_size[0], stepsize) xstops = range(tilesize, array_size[0], stepsize) xtiles = list(zip(xstarts, xstops)) xtiles.append((xtiles[-1][0] + stepsize, array_size[0])) tiles = itertools.product(xtiles, ytiles) for tile in tiles: # xstart, ystart, xcount, ycount xstart = tile[0][0] ystart = tile[1][0] xstop = tile[0][1] ystop = tile[1][1] pixels = [xstart, ystart, xstop - xstart, ystop - ystart] array = self.geodata.read_array(pixels=pixels) xystart = [xstart, ystart] self.extract_features(array, xystart, *args, **kwargs) def load_features(self, in_path, format='npy'): """ Loading @@ -314,10 +384,10 @@ class Node(dict, MutableMapping): keypoints, descriptors = io_keypoints.from_hdf(in_path, key=self['image_name']) self._keypoints = keypoints self.keypoints = keypoints self.descriptors = descriptors def save_features(self, out_path, format='npy'): def save_features(self, out_path): """ Save the extracted keypoints and descriptors to the given HDF5 file. By default, the .npz files are saved Loading @@ -332,18 +402,13 @@ class Node(dict, MutableMapping): The desired output format. """ if not hasattr(self, '_keypoints'): warnings.warn('Node {} has not had features extracted.'.format(i)) if self.keypoints.empty: warnings.warn('Node {} has not had features extracted.'.format(self['node_id'])) return if format == 'hdf': io_keypoints.to_hdf(self._keypoints, self.descriptors, out_path, key=self['image_name']) elif format == 'npy': io_keypoints.to_npy(self._keypoints, self.descriptors, io_keypoints.to_npy(self.keypoints, self.descriptors, out_path) else: warnings.warn('Unknown keypoint output format.') def group_correspondences(self, cg, *args, deepen=False, **kwargs): """ Loading Loading @@ -446,15 +511,15 @@ class Node(dict, MutableMapping): self.point_to_correspondence_df = pd.DataFrame(data, columns=columns) def suppress(self, func=spf.response, **kwargs): if not hasattr(self, '_keypoints'): if not hasattr(self, 'keypoints'): raise AttributeError('No keypoints extracted for this node.') domain = self.handle.raster_size self._keypoints['strength'] = self._keypoints.apply(func, axis=1) self.keypoints['strength'] = self.keypoints.apply(func, axis=1) if not hasattr(self, 'suppression'): # Instantiate a suppression object and suppress keypoints self.suppression = od.SpatialSuppression(self._keypoints, domain, **kwargs) self.suppression = od.SpatialSuppression(self.keypoints, domain, **kwargs) self.suppression.suppress() else: # Update the suppression object attributes and process Loading @@ -463,7 +528,7 @@ class Node(dict, MutableMapping): setattr(self.suppression, k, v) self.suppression.suppress() self.masks = ('suppression', self.suppression.mask) self.masks['suppression'] = self.suppression.mask def coverage_ratio(self, clean_keys=[]): """ Loading @@ -475,11 +540,11 @@ class Node(dict, MutableMapping): The ratio of convex hull area to total area. """ ideal_area = self.geodata.pixel_area if not hasattr(self, '_keypoints'): if not hasattr(self, 'keypoints'): raise AttributeError('Keypoints must be extracted already, they have not been.') matches, mask = self._clean(clean_keys) keypoints = self._keypoints[mask][['x', 'y']].values #TODO: clean_keys are disabled - re-enable. keypoints = self.get_keypoint_coordinates() ratio = convex_hull_ratio(keypoints, ideal_area) return ratio Loading @@ -505,9 +570,9 @@ class Node(dict, MutableMapping): mask : series A boolean series to inflate back to the full match set """ if not hasattr(self, '_keypoints'): if not hasattr(self, 'keypoints'): raise AttributeError('Keypoints have not been extracted for this node.') panel = self.masks mask = panel[clean_keys].all(axis=1) matches = self._keypoints[mask] matches = self.keypoints[mask] return matches, mask autocnet/graph/tests/test_node.py +82 −49 Changes for autocnet/graph/tests/test_node.py: 82 added lines, 49 removed lines. Original line number Diff line number Diff line Loading @@ -2,10 +2,12 @@ import os import sys import unittest from unittest.mock import Mock, MagicMock import warnings import numpy as np import pandas as pd import pytest from autocnet.examples import get_path from plio.io.io_gdal import GeoDataset Loading @@ -15,62 +17,93 @@ from .. import node sys.path.insert(0, os.path.abspath('..')) class TestNode(unittest.TestCase): class TestNode(object): def setUp(self): @pytest.fixture def node(self): img = get_path('AS15-M-0295_SML.png') self.node = node.Node(image_name='AS15-M-0295_SML', return node.Node(image_name='AS15-M-0295_SML', image_path=img) def test_get_handle(self): self.assertIsInstance(self.node.geodata, GeoDataset) def test_get_byte_array(self): image = self.node.get_byte_array() self.assertEqual((1012, 1012), image.shape) self.assertEqual(np.uint8, image.dtype) def test_get_array(self): image = self.node.get_array() self.assertEqual((1012, 1012), image.shape) self.assertEqual(np.float32, image.dtype) def test_extract_features(self): image = self.node.get_array() self.node.extract_features(image, extractor_parameters={'nfeatures': 10}) self.assertEquals(len(self.node.get_keypoints()), 10) self.assertEquals(len(self.node.descriptors), 10) self.assertEqual(10, self.node.nkeypoints) def test_masks(self): # Assert a warning raise here with warnings.catch_warnings(record=True) as w: masks = self.node.masks self.assertEqual(len(w), 1) self.assertEqual(w[0].category, UserWarning) image = self.node.get_array() self.node.extract_features(image, extractor_parameters={'nfeatures': 5}) self.assertIsInstance(self.node.masks, pd.DataFrame) def test_get_handle(self, node): assert isinstance(node.geodata, GeoDataset) def test_get_byte_array(self, node): image = node.get_byte_array() assert (1012, 1012) == image.shape assert np.uint8 == image.dtype def test_get_array(self, node): image = node.get_array() assert (1012, 1012) == image.shape assert np.float32 == image.dtype def test_extract_features(self, node): image = node.get_array() node.extract_features(image, extractor_parameters={'nfeatures': 10}) assert len(node.get_keypoints()) == 10 assert len(node.descriptors) == 10 assert 10 == node.nkeypoints def test_extract_downsampled_features(self, node): # Trust that the img = np.random.random(size=(1000,1000)) geodata = Mock(spec=GeoDataset) geodata.raster_size = img.shape geodata.read_array = MagicMock(return_value=img) node.extract_features_with_downsampling(5, extractor_parameters={'nfeatures':10}) assert len(node.keypoints) == 10 assert node.keypoints['x'].max() > 500 def test_extract_tiled_features(self, node): tilesize = 500 node.extract_features_with_tiling(tilesize=tilesize, overlap=50, extractor_parameters={'nfeatures':10}) kps = node.keypoints assert kps['x'].min() < tilesize assert kps['y'].min() < tilesize assert len(kps) == pytest.approx(90, 3) def test_masks(self, node): image = node.get_array() node.extract_features(image, extractor_parameters={'nfeatures': 5}) assert isinstance(node.masks, pd.DataFrame) # Create an artificial mask self.node.masks = ('foo', np.array([0, 0, 1, 1, 1], dtype=np.bool)) self.assertEqual(self.node.masks['foo'].sum(), 3) node.masks['foo'] = np.array([0, 0, 1, 1, 1], dtype=np.bool) assert node.masks['foo'].sum() == 3 def test_convex_hull_ratio_fail(self): # Convex hull computation is checked lower in the hull computation self.assertRaises(AttributeError, self.node.coverage_ratio) def test_isis_serial(self): serial = self.node.isis_serial self.assertEqual(None, serial) def test_save_load(self): #self.assertRaises(AttributeError, node.coverage_ratio) pass def test_coverage(self): image = self.node.get_array() self.node.extract_features(image, method='sift', extractor_parameters={'nfeatures': 10}) coverage_percn = self.node.coverage() self.assertAlmostEqual(coverage_percn, 38.06139557) def test_isis_serial(self, node): serial = node.isis_serial assert None == serial def test_save_load(self, node, tmpdir): # Test that without keypoints this warns with pytest.warns(UserWarning) as warn: node.save_features(tmpdir.join('noattr.npy')) assert len(warn) == 1 basename = tmpdir.dirname # With keypoints to npy reference = pd.DataFrame(np.arange(10).reshape(5,2), columns=['x', 'y']) node.keypoints = reference tmpdir.join('kps.npz') node.save_features(os.path.join(basename, 'kps.npz')) node.keypoints = None node.load_features(os.path.join(basename, 'kps.npz')) assert node.keypoints.equals(reference) def test_coverage(self, node): image = node.get_array() node.extract_features(image, extractor_method='sift', extractor_parameters={'nfeatures': 10}) coverage_percn = node.coverage() assert coverage_percn == pytest.approx(38.06139557, 2) Loading
autocnet/graph/network.py +70 −16 Changes for autocnet/graph/network.py: 70 added lines, 16 removed lines. Original line number Diff line number Diff line Loading @@ -15,6 +15,12 @@ from autocnet.graph.node import Node from autocnet.io import network as io_network from autocnet.vis.graph_view import plot_graph, cluster_plot # The total number of pixels squared that can fit into the keys number of GB of RAM for SIFT. MAXSIZE = {0:None, 2:6250, 4:8840, 8:12500, 12:15310} class CandidateGraph(nx.Graph): """ Loading Loading @@ -80,6 +86,20 @@ class CandidateGraph(nx.Graph): eq = False return eq @property def maxsize(self): if not hasattr(self, '_maxsize'): self._maxsize = MAXSIZE[0] return self._maxsize @maxsize.setter def maxsize(self, value): if not value in MAXSIZE.keys(): raise KeyError('Value must be in {}'.format(','.join(map(str,MAXSIZE.keys())))) else: self._maxsize = MAXSIZE[value] @classmethod def from_filelist(cls, filelist, basepath=None): """ Loading Loading @@ -197,25 +217,49 @@ class CandidateGraph(nx.Graph): raise NotImplementedError def extract_features(self, *args, **kwargs): def extract_features(self, band=1, *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'. """ for i, node in self.nodes_iter(data=True): array = node.geodata.read_array(band=band) node.extract_features(array, *args, **kwargs), def extract_features_with_downsampling(self, downsample_amount=None, *args, **kwargs): """ Extract interest points from a downsampled array. The array is downsampled by the downsample_amount keyword using the Lanconz downsample amount. If the downsample keyword is not supplied, compute a downsampling constant as the total array size divided by the network maxsize attribute. Parameters ---------- method : {'orb', 'sift', 'fast'} The descriptor method to be used extractor_parameters : dict A dictionary containing OpenCV SIFT parameters names and values. downsampling : int The divisor to image_size to down sample the input image. downsample_amount : int The amount of downsampling to apply to the image """ for i, node in self.nodes_iter(data=True): image = node.get_array() node.extract_features(image, *args, **kwargs), if downsample_amount == None: total_size = node.geodata.raster_size[0] * node.geodata.raster_size[1] downsample_amount = math.ceil(total_size / self.maxsize**2) node.extract_features_with_downsampling(downsample_amount, *args, **kwargs) def extract_features_with_tiling(self, tilesize=1000, overlap=500, *args, **kwargs): for i, node in self.nodes_iter(data=True): node.extract_features_with_tiling(tilesize=tilesize, overlap=overlap, *args, **kwargs) def extract_subsets(self, *args, **kwargs): """ Extracts features from each image in those regions estimated to be overlapping. *args and **kwargs are passed to the feature extractor. For example, passing method='sift' will cause the extractor to use the sift method. """ for source, destination, e in self.edges_iter(data=True): e.extract_subset(*args, **kwargs) def save_features(self, out_path, nodes=[], **kwargs): """ Loading Loading @@ -279,6 +323,17 @@ class CandidateGraph(nx.Graph): """ self.apply_func_to_edges('decompose_and_match', *args, **kwargs) def estimate_mbrs(self, *args, **kwargs): """ For each edge, estimate the overlap and compute a minimum bounding rectangle (mbr) in pixel space. See Also -------- autocnet.graoh.edge.Edge.compute_mbr """ self.apply_func_to_edges('estimate_mbr', *args, **kwargs) def compute_clusters(self, func=markov_cluster.mcl, *args, **kwargs): """ Apply some graph clustering algorithm to compute a subset of the global Loading Loading @@ -374,7 +429,7 @@ class CandidateGraph(nx.Graph): See Also -------- autocnet.matcher.outlier_detector.DistanceRatio.compute autocnet.matcher.cpu_outlier_detector.DistanceRatio.compute ''' self.apply_func_to_edges('ratio_check', *args, **kwargs) Loading @@ -385,7 +440,7 @@ class CandidateGraph(nx.Graph): See Also -------- autocnet.graph.edge.Edge.compute_homography autocnet.matcher.outlier_detector.compute_homography autocnet.matcher.cpu_outlier_detector.compute_homography ''' self.apply_func_to_edges('compute_homography', *args, **kwargs) Loading @@ -395,7 +450,7 @@ class CandidateGraph(nx.Graph): See Also -------- autocnet.matcher.outlier_detector.compute_fundamental_matrix autocnet.matcher.cpu_outlier_detector.compute_fundamental_matrix ''' self.apply_func_to_edges('compute_fundamental_matrix', *args, **kwargs) Loading @@ -415,7 +470,7 @@ class CandidateGraph(nx.Graph): See Also -------- autocnet.matcher.outlier_detector.SpatialSuppression autocnet.matcher.cpu_outlier_detector.SpatialSuppression ''' self.apply_func_to_edges('suppress', *args, **kwargs) Loading Loading @@ -644,8 +699,7 @@ class CandidateGraph(nx.Graph): # get all edges that have matches matches = [(u, v) for u, v, edge in self.edges_iter(data=True) if hasattr(edge, 'matches') and not edge.matches is None] if not edge.matches.empty] return self.create_edge_subgraph(matches) Loading
autocnet/graph/node.py +123 −58 Changes for autocnet/graph/node.py: 123 added lines, 58 removed lines. Original line number Diff line number Diff line from collections import defaultdict, MutableMapping import itertools import os import warnings Loading @@ -6,7 +7,7 @@ import numpy as np import pandas as pd from plio.io.io_gdal import GeoDataset from plio.io.isis_serial_number import generate_serial_number from scipy.misc import bytescale from scipy.misc import bytescale, imresize from autocnet.cg import cg from autocnet.control.control import Correspondence, Point Loading @@ -14,8 +15,8 @@ from autocnet.control.control import Correspondence, Point from autocnet.io import keypoints as io_keypoints from autocnet.matcher.add_depth import deepen_correspondences from autocnet.matcher import feature_extractor as fe from autocnet.matcher import outlier_detector as od from autocnet.matcher import cpu_extractor as fe from autocnet.matcher import cpu_outlier_detector as od from autocnet.matcher import suppression_funcs as spf from autocnet.cg.cg import convex_hull_ratio Loading Loading @@ -67,6 +68,8 @@ class Node(dict, MutableMapping): self.point_to_correspondence = defaultdict(set) self.point_to_correspondence_df = None self.descriptors = None self.keypoints = pd.DataFrame() self.masks = pd.DataFrame() def __repr__(self): return """ Loading @@ -87,11 +90,9 @@ class Node(dict, MutableMapping): if isinstance(v, pd.DataFrame): if not v.equals(o[k]): eq = False print('N', k) elif isinstance(v, np.ndarray): if not v.all() == o[k].all(): eq = False print('N2', k) return eq """ def __getitem__(self, item): Loading Loading @@ -119,16 +120,16 @@ class Node(dict, MutableMapping): else: return None @property """ @property def masks(self): mask_lookup = {'suppression': 'suppression'} if not hasattr(self, '_keypoints'): if self.keypoints is None: warnings.warn('Keypoints have not been extracted') return if not hasattr(self, '_masks'): self._masks = pd.DataFrame(index=self._keypoints.index) self._masks = pd.DataFrame(index=self.keypoints.index) # If the mask is coming form another object that tracks # state, dynamically draw the mask from the object. Loading @@ -142,7 +143,7 @@ class Node(dict, MutableMapping): column_name = v[0] boolean_mask = v[1] self.masks[column_name] = boolean_mask """ @property def isis_serial(self): """ Loading @@ -159,24 +160,7 @@ class Node(dict, MutableMapping): @property def nkeypoints(self): if hasattr(self, '_keypoints'): return len(self._keypoints) else: return 0 """ @property def keypoints(self): if hasattr(self, '_keypoints'): return self._keypoints.copy() else: return None @property def descriptors(self): if hasattr(self, '_descriptors'): return np.copy(self._descriptors) else: return None""" return len(self.keypoints) def coverage(self): """ Loading Loading @@ -235,23 +219,19 @@ class Node(dict, MutableMapping): """ Return the keypoints for the node. If index is passed, return the appropriate subset. Parameters ---------- index : iterable indices for of the keypoints to return Returns ------- : dataframe A pandas dataframe of keypoints """ if hasattr(self, '_keypoints'): if index is not None: return self._keypoints.loc[index] return self.keypoints.loc[index] else: return self._keypoints return self.keypoints def get_keypoint_coordinates(self, index=None, homogeneous=False): """ Loading @@ -271,7 +251,10 @@ class Node(dict, MutableMapping): : dataframe A pandas dataframe of keypoint coordinates """ keypoints = self.get_keypoints(index=index)[['x', 'y']] if index is None: keypoints = self.keypoints[['x', 'y']] else: keypoints = self.keypoints.loc[index][['x', 'y']] if homogeneous: keypoints['homogeneous'] = 1 Loading @@ -279,7 +262,7 @@ class Node(dict, MutableMapping): return keypoints @staticmethod def _extract_features(*args, **kwargs): def _extract_features(array, *args, **kwargs): """ Extract features for the node Loading @@ -288,13 +271,100 @@ class Node(dict, MutableMapping): array : ndarray kwargs : dict kwargs passed to autocnet.feature_extractor.extract_features kwargs passed to autocnet.cpu_extractor.extract_features """ pass def extract_features(self, *args, **kwargs): self._keypoints, self.descriptors = Node._extract_features(*args, **kwargs) def extract_features(self, array, xystart=[], *args, **kwargs): arraysize = array.shape[0] * array.shape[1] try: maxsize = self.maxsize[0] * self.maxsize[1] except: maxsize = np.inf if arraysize > maxsize: warnings.warn('Node: {}. Maximum feature extraction array size is {}. Maximum array size is {}. Please use tiling or downsampling.'.format(self['node_id'], maxsize, arraysize)) keypoints, descriptors = Node._extract_features(array, *args, **kwargs) count = len(self.keypoints) if xystart: keypoints['x'] += xystart[0] keypoints['y'] += xystart[1] self.keypoints = pd.concat((self.keypoints, keypoints)) descriptor_mask = self.keypoints.duplicated()[count:] number_new = descriptor_mask.sum() # Removed duplicated and re-index the merged keypoints self.keypoints.drop_duplicates(inplace=True) self.keypoints.reset_index(inplace=True, drop=True) if self.descriptors is not None: self.descriptors = np.concatenate((self.descriptors, descriptors[~descriptor_mask])) else: self.descriptors = descriptors def extract_features_from_overlaps(self, overlaps=[], downsampling=False, tiling=False, *args, **kwargs): # iterate through the overlaps # check for downsampling or tiling and dispatch as needed to that func # that should then dispatch to the extract features func pass def extract_features_with_downsampling(self, downsample_amount, array_read_args={}, interp='lanczos', *args, **kwargs): """ Extract interest points for the this node (image) by first downsampling, then applying the extractor, and then upsampling the results backin to true image space. Parameters ---------- downsample_amount : int The amount to downsample by """ array_size = self.geodata.raster_size total_size = array_size[0] * array_size[1] shape = (int(array_size[0] / downsample_amount), int(array_size[1] / downsample_amount)) array = imresize(self.geodata.read_array(**array_read_args), shape, interp=interp) self.extract_features(array, *args, **kwargs) self.keypoints['x'] *= downsample_amount self.keypoints['y'] *= downsample_amount def extract_features_with_tiling(self, tilesize=1000, overlap=500, *args, **kwargs): array_size = self.geodata.raster_size stepsize = tilesize - overlap if stepsize < 0: raise ValueError('Overlap can not be greater than tilesize.') # Compute the tiles ystarts = range(0, array_size[1], stepsize) ystops = range(tilesize, array_size[1], stepsize) ytiles = list(zip(ystarts, ystops)) ytiles.append((ytiles[-1][0] + stepsize, array_size[1])) xstarts = range(0, array_size[0], stepsize) xstops = range(tilesize, array_size[0], stepsize) xtiles = list(zip(xstarts, xstops)) xtiles.append((xtiles[-1][0] + stepsize, array_size[0])) tiles = itertools.product(xtiles, ytiles) for tile in tiles: # xstart, ystart, xcount, ycount xstart = tile[0][0] ystart = tile[1][0] xstop = tile[0][1] ystop = tile[1][1] pixels = [xstart, ystart, xstop - xstart, ystop - ystart] array = self.geodata.read_array(pixels=pixels) xystart = [xstart, ystart] self.extract_features(array, xystart, *args, **kwargs) def load_features(self, in_path, format='npy'): """ Loading @@ -314,10 +384,10 @@ class Node(dict, MutableMapping): keypoints, descriptors = io_keypoints.from_hdf(in_path, key=self['image_name']) self._keypoints = keypoints self.keypoints = keypoints self.descriptors = descriptors def save_features(self, out_path, format='npy'): def save_features(self, out_path): """ Save the extracted keypoints and descriptors to the given HDF5 file. By default, the .npz files are saved Loading @@ -332,18 +402,13 @@ class Node(dict, MutableMapping): The desired output format. """ if not hasattr(self, '_keypoints'): warnings.warn('Node {} has not had features extracted.'.format(i)) if self.keypoints.empty: warnings.warn('Node {} has not had features extracted.'.format(self['node_id'])) return if format == 'hdf': io_keypoints.to_hdf(self._keypoints, self.descriptors, out_path, key=self['image_name']) elif format == 'npy': io_keypoints.to_npy(self._keypoints, self.descriptors, io_keypoints.to_npy(self.keypoints, self.descriptors, out_path) else: warnings.warn('Unknown keypoint output format.') def group_correspondences(self, cg, *args, deepen=False, **kwargs): """ Loading Loading @@ -446,15 +511,15 @@ class Node(dict, MutableMapping): self.point_to_correspondence_df = pd.DataFrame(data, columns=columns) def suppress(self, func=spf.response, **kwargs): if not hasattr(self, '_keypoints'): if not hasattr(self, 'keypoints'): raise AttributeError('No keypoints extracted for this node.') domain = self.handle.raster_size self._keypoints['strength'] = self._keypoints.apply(func, axis=1) self.keypoints['strength'] = self.keypoints.apply(func, axis=1) if not hasattr(self, 'suppression'): # Instantiate a suppression object and suppress keypoints self.suppression = od.SpatialSuppression(self._keypoints, domain, **kwargs) self.suppression = od.SpatialSuppression(self.keypoints, domain, **kwargs) self.suppression.suppress() else: # Update the suppression object attributes and process Loading @@ -463,7 +528,7 @@ class Node(dict, MutableMapping): setattr(self.suppression, k, v) self.suppression.suppress() self.masks = ('suppression', self.suppression.mask) self.masks['suppression'] = self.suppression.mask def coverage_ratio(self, clean_keys=[]): """ Loading @@ -475,11 +540,11 @@ class Node(dict, MutableMapping): The ratio of convex hull area to total area. """ ideal_area = self.geodata.pixel_area if not hasattr(self, '_keypoints'): if not hasattr(self, 'keypoints'): raise AttributeError('Keypoints must be extracted already, they have not been.') matches, mask = self._clean(clean_keys) keypoints = self._keypoints[mask][['x', 'y']].values #TODO: clean_keys are disabled - re-enable. keypoints = self.get_keypoint_coordinates() ratio = convex_hull_ratio(keypoints, ideal_area) return ratio Loading @@ -505,9 +570,9 @@ class Node(dict, MutableMapping): mask : series A boolean series to inflate back to the full match set """ if not hasattr(self, '_keypoints'): if not hasattr(self, 'keypoints'): raise AttributeError('Keypoints have not been extracted for this node.') panel = self.masks mask = panel[clean_keys].all(axis=1) matches = self._keypoints[mask] matches = self.keypoints[mask] return matches, mask
autocnet/graph/tests/test_node.py +82 −49 Changes for autocnet/graph/tests/test_node.py: 82 added lines, 49 removed lines. Original line number Diff line number Diff line Loading @@ -2,10 +2,12 @@ import os import sys import unittest from unittest.mock import Mock, MagicMock import warnings import numpy as np import pandas as pd import pytest from autocnet.examples import get_path from plio.io.io_gdal import GeoDataset Loading @@ -15,62 +17,93 @@ from .. import node sys.path.insert(0, os.path.abspath('..')) class TestNode(unittest.TestCase): class TestNode(object): def setUp(self): @pytest.fixture def node(self): img = get_path('AS15-M-0295_SML.png') self.node = node.Node(image_name='AS15-M-0295_SML', return node.Node(image_name='AS15-M-0295_SML', image_path=img) def test_get_handle(self): self.assertIsInstance(self.node.geodata, GeoDataset) def test_get_byte_array(self): image = self.node.get_byte_array() self.assertEqual((1012, 1012), image.shape) self.assertEqual(np.uint8, image.dtype) def test_get_array(self): image = self.node.get_array() self.assertEqual((1012, 1012), image.shape) self.assertEqual(np.float32, image.dtype) def test_extract_features(self): image = self.node.get_array() self.node.extract_features(image, extractor_parameters={'nfeatures': 10}) self.assertEquals(len(self.node.get_keypoints()), 10) self.assertEquals(len(self.node.descriptors), 10) self.assertEqual(10, self.node.nkeypoints) def test_masks(self): # Assert a warning raise here with warnings.catch_warnings(record=True) as w: masks = self.node.masks self.assertEqual(len(w), 1) self.assertEqual(w[0].category, UserWarning) image = self.node.get_array() self.node.extract_features(image, extractor_parameters={'nfeatures': 5}) self.assertIsInstance(self.node.masks, pd.DataFrame) def test_get_handle(self, node): assert isinstance(node.geodata, GeoDataset) def test_get_byte_array(self, node): image = node.get_byte_array() assert (1012, 1012) == image.shape assert np.uint8 == image.dtype def test_get_array(self, node): image = node.get_array() assert (1012, 1012) == image.shape assert np.float32 == image.dtype def test_extract_features(self, node): image = node.get_array() node.extract_features(image, extractor_parameters={'nfeatures': 10}) assert len(node.get_keypoints()) == 10 assert len(node.descriptors) == 10 assert 10 == node.nkeypoints def test_extract_downsampled_features(self, node): # Trust that the img = np.random.random(size=(1000,1000)) geodata = Mock(spec=GeoDataset) geodata.raster_size = img.shape geodata.read_array = MagicMock(return_value=img) node.extract_features_with_downsampling(5, extractor_parameters={'nfeatures':10}) assert len(node.keypoints) == 10 assert node.keypoints['x'].max() > 500 def test_extract_tiled_features(self, node): tilesize = 500 node.extract_features_with_tiling(tilesize=tilesize, overlap=50, extractor_parameters={'nfeatures':10}) kps = node.keypoints assert kps['x'].min() < tilesize assert kps['y'].min() < tilesize assert len(kps) == pytest.approx(90, 3) def test_masks(self, node): image = node.get_array() node.extract_features(image, extractor_parameters={'nfeatures': 5}) assert isinstance(node.masks, pd.DataFrame) # Create an artificial mask self.node.masks = ('foo', np.array([0, 0, 1, 1, 1], dtype=np.bool)) self.assertEqual(self.node.masks['foo'].sum(), 3) node.masks['foo'] = np.array([0, 0, 1, 1, 1], dtype=np.bool) assert node.masks['foo'].sum() == 3 def test_convex_hull_ratio_fail(self): # Convex hull computation is checked lower in the hull computation self.assertRaises(AttributeError, self.node.coverage_ratio) def test_isis_serial(self): serial = self.node.isis_serial self.assertEqual(None, serial) def test_save_load(self): #self.assertRaises(AttributeError, node.coverage_ratio) pass def test_coverage(self): image = self.node.get_array() self.node.extract_features(image, method='sift', extractor_parameters={'nfeatures': 10}) coverage_percn = self.node.coverage() self.assertAlmostEqual(coverage_percn, 38.06139557) def test_isis_serial(self, node): serial = node.isis_serial assert None == serial def test_save_load(self, node, tmpdir): # Test that without keypoints this warns with pytest.warns(UserWarning) as warn: node.save_features(tmpdir.join('noattr.npy')) assert len(warn) == 1 basename = tmpdir.dirname # With keypoints to npy reference = pd.DataFrame(np.arange(10).reshape(5,2), columns=['x', 'y']) node.keypoints = reference tmpdir.join('kps.npz') node.save_features(os.path.join(basename, 'kps.npz')) node.keypoints = None node.load_features(os.path.join(basename, 'kps.npz')) assert node.keypoints.equals(reference) def test_coverage(self, node): image = node.get_array() node.extract_features(image, extractor_method='sift', extractor_parameters={'nfeatures': 10}) coverage_percn = node.coverage() assert coverage_percn == pytest.approx(38.06139557, 2)