Loading autocnet/graph/edge.py +6 −0 Original line number Diff line number Diff line Loading @@ -555,7 +555,13 @@ class Edge(dict, MutableMapping): Estimate a source and destination minimum bounding rectangle, in pixel space """ try: self.overlap_latlon_coords, self["source_mbr"], self["destin_mbr"] = self.source.geodata.compute_overlap(self.destination.geodata, **kwargs) except Exception as e: raise Exception("Overlap between {} and {} could not be " "computed: {}".format(self.source['image_name'], self.destination['image_name'], type(e))) def get_matches(self): # pragma: no cover if self.matches.empty: Loading autocnet/graph/network.py +187 −5 Original line number Diff line number Diff line Loading @@ -32,6 +32,7 @@ MAXSIZE = {0:None, 8: 12500, 12: 15310} class CandidateGraph(nx.Graph): """ A NetworkX derived directed graph to store candidate overlap images. Loading Loading @@ -165,7 +166,7 @@ class CandidateGraph(nx.Graph): adjacency_dict[i.file_name].append(j.file_name) adjacency_dict[j.file_name].append(i.file_name) except: warnings.warn('Failed to calculated intersection between {} and {}'.format(i, j)) warnings.warn('Failed to calculate intersection between {} and {}'.format(i, j)) return cls(adjacency_dict) Loading Loading @@ -223,16 +224,198 @@ class CandidateGraph(nx.Graph): """ return self.node[node_index]['image_name'] def add_image(self, *args, **kwargs): def add_image(self, image_name, adjacency=None, basepath=None, apply_func=None): """ Adds an image node to the graph. Parameters ---------- image_name : str The file name of or path to the image to add adjacency : string or Node list The list of adjacent Nodes or image files for this image basepath : str The directory path for the image apply_func : function A static function that takes an Edge as its parameter Function will be applied to all Edges generated when adding the image """ def add_node(img_pth): """ Adds a new, disconnected Node to the graph and returns a reference to it img_pth : The absolute path to the image Returns ------- Node : A reference to the added Node """ raise NotImplementedError self._order_adjacency() # Check that image path exists try: assert os.path.exists(img_pth) except AssertionError: raise FileNotFoundError("Could not add {} to CandidateGraph; " "File does not exist".format(img_pth)) # Grab the image name img_nm = os.path.basename(img_pth) # Get the node id & map [id -> Node] in the graph id = self.graph['node_counter'] node = Node(img_nm, img_pth, id) self.node[id] = node # Map [image name -> id] in the graph and increment node counter self.graph['node_name_map'][img_nm] = id self.graph['node_counter'] += 1 # Return the Node reference return node # Check if image is already in the graph if image_name in self.graph['node_name_map']: warnings.warn("{} is already in the graph".format(image_name)) return # Basepath resolution if basepath: image_path = os.path.join(basepath, image_name) else: image_path = image_name image_name = os.path.basename(image_path) # Create new node within graph new_node = add_node(image_path) # If adjacency supplied make sure it's the right type if adjacency: # Type check try: assert type(adjacency) is list except AssertionError: raise TypeError("Named parameter 'adjacency' must be a list of" "adjacent Node objects or list of adjacent " "images; Could not add {} to " "CandidateGraph".format(image_name)) # If adjacency not supplied, figure it out from footprints else: # Create empty adjacency list adjacency = list() # Make sure new node has valid footprint; If not, it will be a # disconnected node on the graph if not new_node.geodata.footprint or not \ new_node.geodata.footprint.IsValid(): warnings.warn('Missing or invalid geospatial data for ' '{0}; {0} will be added to the CandidateGraph' 'as a disconnected Node'.format(image_name)) return # Detect adjacency between our new node and the CG's nodes target_nodes = [self.node[idx] for idx in self.nodes()] valid_datasets = list() datasets = [node.geodata for node in target_nodes] # Make sure target nodes have valid footprints for ds in datasets: # Skip the source node if it's in the list of target nodes if ds.file_name == new_node['image_path']: continue # Grab footprints from nodes that have them fp = ds.footprint if fp and fp.IsValid(): valid_datasets.append(ds) else: warnings.warn('Missing or invalid geospatial data for ' '{}'.format(os.path.basename(ds.file_name))) # Grab the footprints and test for intersection for ds in valid_datasets: ds_file_name = os.path.basename(ds.file_name) try: if new_node.geodata.footprint.Intersects(ds.footprint): adjacency.append(ds_file_name) except: warnings.warn('Failed to calculate intersection between {} ' 'and {}'.format(image_name, ds_file_name)) # Build new edge(s) from adjacency for a_img in adjacency: # If string (image name) if isinstance(a_img, str): # If adjacent img is already in the graph if a_img in self.graph['node_name_map'].keys(): # Set the nodes for the new edge a_node_idx = self.graph['node_name_map'][a_img] s = self.node[a_node_idx] d = new_node # If adjacent img isnt already in graph, add it else: # Set the nodes for the new graph s = new_node d = add_node(os.path.join(basepath, a_img)) # If Node elif isinstance(a_img, Node): # If it's already in the graph, it'll be the source node, # since its idx is lower than our new node if a_img['image_name'] in [self.node[idx]['image_name'] for idx in self.nodes()]: s = a_img d = new_node # Otherwise, can't create edge else: warnings.warn("{0} is not in the graph; No Edge between" "{0} and {1} can be " "created".format(a_img['image_name'], image_name)) continue else: raise TypeError("Adjacency list contains Node objects or image " "names; Could not add {} to " "CandidateGraph".format(image_name)) # Create the new edge new_edge = Edge(s, d) # If there's a clean func for the new edge, apply it if apply_func: ERR = "Named parameter 'apply_func' must be a static " \ "function or list of static functions; These " \ "function(s) are applied to all new edges generated " \ "when adding {0}; Could not add {0} to " \ "CandidateGraph".format(image_name) # Type Check try: assert callable(apply_func) or type(apply_func) is list # If it's a function, apply it if callable(apply_func): apply_func(new_edge) # If it's a list of functions, apply all of them else: [func(new_edge) for func in apply_func] except AssertionError: raise TypeError(ERR) # Grab node ids s_id = s['node_id'] d_id = d['node_id'] # Make sure source node is a key in the edge lookup if s_id not in self.edge.keys(): self.edge[s_id] = dict() # Add the new edge to the graph self.edge[s_id][d_id] = new_edge def extract_features(self, band=1, *args, **kwargs): # pragma: no cover """ Loading Loading @@ -440,7 +623,6 @@ class CandidateGraph(nx.Graph): if any(return_lis): return return_lis def apply(self, function, on='edge',out=None, args=(), **kwargs): """ Applys a function to every node or edge, returns collected return Loading autocnet/graph/tests/test_network.py +109 −2 Original line number Diff line number Diff line Loading @@ -57,8 +57,115 @@ def test_size(graph): def test_add_image(graph): with pytest.raises(NotImplementedError): graph.add_image() # apply_func def extract_and_match(edge): for n in [edge.source, edge.destination]: n.extract_features(n.get_array(band=1), extractor_parameters={'nfeatures': 800}) edge.match() basepath = get_path('Apollo15') cube_adjacency = {"AS15-M-0297_crop.cub": ["AS15-M-0298_crop.cub"], "AS15-M-0298_crop.cub": ["AS15-M-0297_crop.cub"]} cang = network.CandidateGraph.from_adjacency(cube_adjacency, basepath=basepath) # Test with all optional args cub_img = "AS15-M-0299_crop.cub" png_img = "AS15-M-0299_SML.png" cub_adj = ["AS15-M-0298_crop.cub", "AS15-M-0297_crop.cub"] png_adj = ["AS15-M-0298_crop.cub", "AS15-M-0297_crop.cub"] cang.add_image(cub_img, adjacency=cub_adj, basepath=basepath, apply_func=extract_and_match) # Assert everything worked properly assert cub_img in cang.graph['node_name_map'].keys() new_node_idx = cang.graph['node_name_map'][cub_img] assert new_node_idx in cang.node.keys() assert cang.node[new_node_idx]['image_name'] == cub_img assert sorted(cang.nodes()) == [0, 1, 2] assert sorted(cang.edges()) == [(0, 1), (0, 2), (1, 2)] assert cang[0][2].destination['image_name'] == cang.edge[0][2].destination['image_name'] == cub_img assert cang[1][2].destination['image_name'] == cang.edge[1][2].destination['image_name'] == cub_img # Test when img is already in graph cang = network.CandidateGraph.from_adjacency(cube_adjacency, basepath=basepath) cang.add_image(cub_adj[0], basepath=basepath) # Test for file not found with pytest.raises(FileNotFoundError): cang = network.CandidateGraph.from_adjacency(cube_adjacency, basepath=basepath) cang.add_image(cub_img, basepath=None) # Test with auto-detect adjacency cang = network.CandidateGraph.from_adjacency(cube_adjacency, basepath=basepath) cang.add_image(cub_img, basepath=basepath) # Test auto-detect when there are nodes w/ invalid geospacial data cang = network.CandidateGraph.from_adjacency(cube_adjacency, basepath=basepath) cang.add_image(png_img, adjacency=png_adj, basepath=basepath) # Invalid cang.add_image(cub_img, basepath=basepath) # Autodetect # Test auto-detect when new node does not intersect # Need a non-intersecting cube file # Test when adjacency is list of nodes cang = network.CandidateGraph.from_adjacency(cube_adjacency, basepath=basepath) cub_adj2 = [cang.node[0], cang.node[1]] cang.add_image(cub_img, adjacency=cub_adj2, basepath=basepath) # Test when an adjacency node is not in graph cang = network.CandidateGraph.from_adjacency(cube_adjacency, basepath=basepath) cub_adj2 = [cang.node[0], cang.node[1]] not_there = node.Node("_" + cub_img, os.path.join(basepath, cub_img), 15) adj = [not_there] edges_bf = cang.edges() cang.add_image(cub_img, adjacency=adj, basepath=basepath) assert cang.edges() == edges_bf # Should be no change in edges # Test when adjacency is of wrong type with pytest.raises(TypeError): cang = network.CandidateGraph.from_adjacency(cube_adjacency, basepath=basepath) cang.add_image(png_img, adjacency=1, basepath=basepath) # Invalid with pytest.raises(TypeError): cang = network.CandidateGraph.from_adjacency(cube_adjacency, basepath=basepath) cang.add_image(png_img, adjacency=[1], basepath=basepath) # Invalid # Test when no adjacency supplied, but image doesn't have footprint; # This results in a disconnected node added to the graph cang = network.CandidateGraph.from_adjacency(cube_adjacency, basepath=basepath) edges_bf = cang.edges() cang.add_image(png_img, basepath=basepath) assert cang.edges() == edges_bf # Assert no change in edges # Test when adjacency includes a node not already in the graph adj = cub_adj adj.append("AS15-M-0300_crop.cub") cang = network.CandidateGraph.from_adjacency(cube_adjacency, basepath=basepath) cang.add_image(cub_img, adjacency=adj, basepath=basepath) # Test when apply_func is not a function / list of functions with pytest.raises(TypeError): cang = network.CandidateGraph.from_adjacency(cube_adjacency, basepath=basepath) cang.add_image(cub_img, adjacency=cub_adj, basepath=basepath, apply_func=1) with pytest.raises(TypeError): cang = network.CandidateGraph.from_adjacency(cube_adjacency, basepath=basepath) cang.add_image(cub_img, adjacency=cub_adj, basepath=basepath, apply_func=[extract_and_match, 1]) def test_island_nodes(disconnected_graph): Loading Loading
autocnet/graph/edge.py +6 −0 Original line number Diff line number Diff line Loading @@ -555,7 +555,13 @@ class Edge(dict, MutableMapping): Estimate a source and destination minimum bounding rectangle, in pixel space """ try: self.overlap_latlon_coords, self["source_mbr"], self["destin_mbr"] = self.source.geodata.compute_overlap(self.destination.geodata, **kwargs) except Exception as e: raise Exception("Overlap between {} and {} could not be " "computed: {}".format(self.source['image_name'], self.destination['image_name'], type(e))) def get_matches(self): # pragma: no cover if self.matches.empty: Loading
autocnet/graph/network.py +187 −5 Original line number Diff line number Diff line Loading @@ -32,6 +32,7 @@ MAXSIZE = {0:None, 8: 12500, 12: 15310} class CandidateGraph(nx.Graph): """ A NetworkX derived directed graph to store candidate overlap images. Loading Loading @@ -165,7 +166,7 @@ class CandidateGraph(nx.Graph): adjacency_dict[i.file_name].append(j.file_name) adjacency_dict[j.file_name].append(i.file_name) except: warnings.warn('Failed to calculated intersection between {} and {}'.format(i, j)) warnings.warn('Failed to calculate intersection between {} and {}'.format(i, j)) return cls(adjacency_dict) Loading Loading @@ -223,16 +224,198 @@ class CandidateGraph(nx.Graph): """ return self.node[node_index]['image_name'] def add_image(self, *args, **kwargs): def add_image(self, image_name, adjacency=None, basepath=None, apply_func=None): """ Adds an image node to the graph. Parameters ---------- image_name : str The file name of or path to the image to add adjacency : string or Node list The list of adjacent Nodes or image files for this image basepath : str The directory path for the image apply_func : function A static function that takes an Edge as its parameter Function will be applied to all Edges generated when adding the image """ def add_node(img_pth): """ Adds a new, disconnected Node to the graph and returns a reference to it img_pth : The absolute path to the image Returns ------- Node : A reference to the added Node """ raise NotImplementedError self._order_adjacency() # Check that image path exists try: assert os.path.exists(img_pth) except AssertionError: raise FileNotFoundError("Could not add {} to CandidateGraph; " "File does not exist".format(img_pth)) # Grab the image name img_nm = os.path.basename(img_pth) # Get the node id & map [id -> Node] in the graph id = self.graph['node_counter'] node = Node(img_nm, img_pth, id) self.node[id] = node # Map [image name -> id] in the graph and increment node counter self.graph['node_name_map'][img_nm] = id self.graph['node_counter'] += 1 # Return the Node reference return node # Check if image is already in the graph if image_name in self.graph['node_name_map']: warnings.warn("{} is already in the graph".format(image_name)) return # Basepath resolution if basepath: image_path = os.path.join(basepath, image_name) else: image_path = image_name image_name = os.path.basename(image_path) # Create new node within graph new_node = add_node(image_path) # If adjacency supplied make sure it's the right type if adjacency: # Type check try: assert type(adjacency) is list except AssertionError: raise TypeError("Named parameter 'adjacency' must be a list of" "adjacent Node objects or list of adjacent " "images; Could not add {} to " "CandidateGraph".format(image_name)) # If adjacency not supplied, figure it out from footprints else: # Create empty adjacency list adjacency = list() # Make sure new node has valid footprint; If not, it will be a # disconnected node on the graph if not new_node.geodata.footprint or not \ new_node.geodata.footprint.IsValid(): warnings.warn('Missing or invalid geospatial data for ' '{0}; {0} will be added to the CandidateGraph' 'as a disconnected Node'.format(image_name)) return # Detect adjacency between our new node and the CG's nodes target_nodes = [self.node[idx] for idx in self.nodes()] valid_datasets = list() datasets = [node.geodata for node in target_nodes] # Make sure target nodes have valid footprints for ds in datasets: # Skip the source node if it's in the list of target nodes if ds.file_name == new_node['image_path']: continue # Grab footprints from nodes that have them fp = ds.footprint if fp and fp.IsValid(): valid_datasets.append(ds) else: warnings.warn('Missing or invalid geospatial data for ' '{}'.format(os.path.basename(ds.file_name))) # Grab the footprints and test for intersection for ds in valid_datasets: ds_file_name = os.path.basename(ds.file_name) try: if new_node.geodata.footprint.Intersects(ds.footprint): adjacency.append(ds_file_name) except: warnings.warn('Failed to calculate intersection between {} ' 'and {}'.format(image_name, ds_file_name)) # Build new edge(s) from adjacency for a_img in adjacency: # If string (image name) if isinstance(a_img, str): # If adjacent img is already in the graph if a_img in self.graph['node_name_map'].keys(): # Set the nodes for the new edge a_node_idx = self.graph['node_name_map'][a_img] s = self.node[a_node_idx] d = new_node # If adjacent img isnt already in graph, add it else: # Set the nodes for the new graph s = new_node d = add_node(os.path.join(basepath, a_img)) # If Node elif isinstance(a_img, Node): # If it's already in the graph, it'll be the source node, # since its idx is lower than our new node if a_img['image_name'] in [self.node[idx]['image_name'] for idx in self.nodes()]: s = a_img d = new_node # Otherwise, can't create edge else: warnings.warn("{0} is not in the graph; No Edge between" "{0} and {1} can be " "created".format(a_img['image_name'], image_name)) continue else: raise TypeError("Adjacency list contains Node objects or image " "names; Could not add {} to " "CandidateGraph".format(image_name)) # Create the new edge new_edge = Edge(s, d) # If there's a clean func for the new edge, apply it if apply_func: ERR = "Named parameter 'apply_func' must be a static " \ "function or list of static functions; These " \ "function(s) are applied to all new edges generated " \ "when adding {0}; Could not add {0} to " \ "CandidateGraph".format(image_name) # Type Check try: assert callable(apply_func) or type(apply_func) is list # If it's a function, apply it if callable(apply_func): apply_func(new_edge) # If it's a list of functions, apply all of them else: [func(new_edge) for func in apply_func] except AssertionError: raise TypeError(ERR) # Grab node ids s_id = s['node_id'] d_id = d['node_id'] # Make sure source node is a key in the edge lookup if s_id not in self.edge.keys(): self.edge[s_id] = dict() # Add the new edge to the graph self.edge[s_id][d_id] = new_edge def extract_features(self, band=1, *args, **kwargs): # pragma: no cover """ Loading Loading @@ -440,7 +623,6 @@ class CandidateGraph(nx.Graph): if any(return_lis): return return_lis def apply(self, function, on='edge',out=None, args=(), **kwargs): """ Applys a function to every node or edge, returns collected return Loading
autocnet/graph/tests/test_network.py +109 −2 Original line number Diff line number Diff line Loading @@ -57,8 +57,115 @@ def test_size(graph): def test_add_image(graph): with pytest.raises(NotImplementedError): graph.add_image() # apply_func def extract_and_match(edge): for n in [edge.source, edge.destination]: n.extract_features(n.get_array(band=1), extractor_parameters={'nfeatures': 800}) edge.match() basepath = get_path('Apollo15') cube_adjacency = {"AS15-M-0297_crop.cub": ["AS15-M-0298_crop.cub"], "AS15-M-0298_crop.cub": ["AS15-M-0297_crop.cub"]} cang = network.CandidateGraph.from_adjacency(cube_adjacency, basepath=basepath) # Test with all optional args cub_img = "AS15-M-0299_crop.cub" png_img = "AS15-M-0299_SML.png" cub_adj = ["AS15-M-0298_crop.cub", "AS15-M-0297_crop.cub"] png_adj = ["AS15-M-0298_crop.cub", "AS15-M-0297_crop.cub"] cang.add_image(cub_img, adjacency=cub_adj, basepath=basepath, apply_func=extract_and_match) # Assert everything worked properly assert cub_img in cang.graph['node_name_map'].keys() new_node_idx = cang.graph['node_name_map'][cub_img] assert new_node_idx in cang.node.keys() assert cang.node[new_node_idx]['image_name'] == cub_img assert sorted(cang.nodes()) == [0, 1, 2] assert sorted(cang.edges()) == [(0, 1), (0, 2), (1, 2)] assert cang[0][2].destination['image_name'] == cang.edge[0][2].destination['image_name'] == cub_img assert cang[1][2].destination['image_name'] == cang.edge[1][2].destination['image_name'] == cub_img # Test when img is already in graph cang = network.CandidateGraph.from_adjacency(cube_adjacency, basepath=basepath) cang.add_image(cub_adj[0], basepath=basepath) # Test for file not found with pytest.raises(FileNotFoundError): cang = network.CandidateGraph.from_adjacency(cube_adjacency, basepath=basepath) cang.add_image(cub_img, basepath=None) # Test with auto-detect adjacency cang = network.CandidateGraph.from_adjacency(cube_adjacency, basepath=basepath) cang.add_image(cub_img, basepath=basepath) # Test auto-detect when there are nodes w/ invalid geospacial data cang = network.CandidateGraph.from_adjacency(cube_adjacency, basepath=basepath) cang.add_image(png_img, adjacency=png_adj, basepath=basepath) # Invalid cang.add_image(cub_img, basepath=basepath) # Autodetect # Test auto-detect when new node does not intersect # Need a non-intersecting cube file # Test when adjacency is list of nodes cang = network.CandidateGraph.from_adjacency(cube_adjacency, basepath=basepath) cub_adj2 = [cang.node[0], cang.node[1]] cang.add_image(cub_img, adjacency=cub_adj2, basepath=basepath) # Test when an adjacency node is not in graph cang = network.CandidateGraph.from_adjacency(cube_adjacency, basepath=basepath) cub_adj2 = [cang.node[0], cang.node[1]] not_there = node.Node("_" + cub_img, os.path.join(basepath, cub_img), 15) adj = [not_there] edges_bf = cang.edges() cang.add_image(cub_img, adjacency=adj, basepath=basepath) assert cang.edges() == edges_bf # Should be no change in edges # Test when adjacency is of wrong type with pytest.raises(TypeError): cang = network.CandidateGraph.from_adjacency(cube_adjacency, basepath=basepath) cang.add_image(png_img, adjacency=1, basepath=basepath) # Invalid with pytest.raises(TypeError): cang = network.CandidateGraph.from_adjacency(cube_adjacency, basepath=basepath) cang.add_image(png_img, adjacency=[1], basepath=basepath) # Invalid # Test when no adjacency supplied, but image doesn't have footprint; # This results in a disconnected node added to the graph cang = network.CandidateGraph.from_adjacency(cube_adjacency, basepath=basepath) edges_bf = cang.edges() cang.add_image(png_img, basepath=basepath) assert cang.edges() == edges_bf # Assert no change in edges # Test when adjacency includes a node not already in the graph adj = cub_adj adj.append("AS15-M-0300_crop.cub") cang = network.CandidateGraph.from_adjacency(cube_adjacency, basepath=basepath) cang.add_image(cub_img, adjacency=adj, basepath=basepath) # Test when apply_func is not a function / list of functions with pytest.raises(TypeError): cang = network.CandidateGraph.from_adjacency(cube_adjacency, basepath=basepath) cang.add_image(cub_img, adjacency=cub_adj, basepath=basepath, apply_func=1) with pytest.raises(TypeError): cang = network.CandidateGraph.from_adjacency(cube_adjacency, basepath=basepath) cang.add_image(cub_img, adjacency=cub_adj, basepath=basepath, apply_func=[extract_and_match, 1]) def test_island_nodes(disconnected_graph): Loading