Commit 53d77c2c authored by jlaura's avatar jlaura Committed by GitHub
Browse files

Merge pull request #225 from evindunn/dev

Added CandidateGraph.add_image()
parents 075ba57e dc5da658
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -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:
+187 −5
Original line number Diff line number Diff line
@@ -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.
@@ -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)

@@ -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
        """
@@ -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
+109 −2
Original line number Diff line number Diff line
@@ -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):