Commit 2cb0fe08 authored by Kristin Berry's avatar Kristin Berry
Browse files

Merge pull request #25 from jcwbacker/graphAccessors

Added convenience methods to CandidateGraph class. Fixes JIRA AUTOCONG-72
parents 6ec490cb 3c68f625
Loading
Loading
Loading
Loading
+25 −1
Original line number Diff line number Diff line
{"AS15-M-0297_SML.png": ["AS15-M-0298_SML.png", "AS15-M-0300_SML.png", "AS15-M-0299_SML.png", "AS15-M-0296_SML.png"], "AS15-M-0300_SML.png": ["AS15-M-0298_SML.png", "AS15-M-0297_SML.png", "AS15-M-0299_SML.png"], "AS15-M-0299_SML.png": ["AS15-M-0298_SML.png", "AS15-M-0297_SML.png", "AS15-M-0300_SML.png", "AS15-M-0296_SML.png"], "AS15-M-0295_SML.png": [], "AS15-M-0296_SML.png": ["AS15-M-0298_SML.png", "AS15-M-0297_SML.png", "AS15-M-0299_SML.png"], "AS15-M-0298_SML.png": ["AS15-M-0297_SML.png", "AS15-M-0300_SML.png", "AS15-M-0299_SML.png", "AS15-M-0296_SML.png"]}
 No newline at end of file
{"autocnet/examples/Apollo15/AS15-M-0297_SML.png"
 : ["autocnet/examples/Apollo15/AS15-M-0298_SML.png",
    "autocnet/examples/Apollo15/AS15-M-0300_SML.png",
    "autocnet/examples/Apollo15/AS15-M-0299_SML.png",
    "autocnet/examples/Apollo15/AS15-M-0296_SML.png"],
 "autocnet/examples/Apollo15/AS15-M-0300_SML.png"
 : ["autocnet/examples/Apollo15/AS15-M-0298_SML.png",
    "autocnet/examples/Apollo15/AS15-M-0297_SML.png",
    "autocnet/examples/Apollo15/AS15-M-0299_SML.png"],
 "autocnet/examples/Apollo15/AS15-M-0299_SML.png"
 : ["autocnet/examples/Apollo15/AS15-M-0298_SML.png",
    "autocnet/examples/Apollo15/AS15-M-0297_SML.png",
    "autocnet/examples/Apollo15/AS15-M-0300_SML.png",
    "autocnet/examples/Apollo15/AS15-M-0296_SML.png"],
 "autocnet/examples/Apollo15/AS15-M-0295_SML.png"
 : [], 
 "autocnet/examples/Apollo15/AS15-M-0296_SML.png"
 : ["autocnet/examples/Apollo15/AS15-M-0298_SML.png",
    "autocnet/examples/Apollo15/AS15-M-0297_SML.png",
    "autocnet/examples/Apollo15/AS15-M-0299_SML.png"],
 "autocnet/examples/Apollo15/AS15-M-0298_SML.png"
 : ["autocnet/examples/Apollo15/AS15-M-0297_SML.png",
    "autocnet/examples/Apollo15/AS15-M-0300_SML.png",
    "autocnet/examples/Apollo15/AS15-M-0299_SML.png",
    "autocnet/examples/Apollo15/AS15-M-0296_SML.png"]}
+4 −2
Original line number Diff line number Diff line
{"AS15-M-0297_SML.png": ["AS15-M-0298_SML.png"], 
"AS15-M-0298_SML.png": ["AS15-M-0297_SML.png"]}
{"autocnet/examples/Apollo15/AS15-M-0297_SML.png"
 : ["autocnet/examples/Apollo15/AS15-M-0298_SML.png"], 
 "autocnet/examples/Apollo15/AS15-M-0298_SML.png"
 : ["autocnet/examples/Apollo15/AS15-M-0297_SML.png"]}
+124 −33
Original line number Diff line number Diff line
@@ -5,9 +5,12 @@ import pandas as pd
import cv2
import numpy as np

from scipy.misc import bytescale # store image array

from autocnet.control.control import C
from autocnet.fileio import io_json

from autocnet.fileio.io_gdal import GeoDataset
from autocnet.matcher import feature_extractor as fe # extract features from image

class CandidateGraph(nx.Graph):
    """
@@ -17,6 +20,12 @@ class CandidateGraph(nx.Graph):
    ----------

    Attributes
    node_counter : int
                   The number of nodes in the graph. 
    node_name_map : dict
                    The mapping of image labels (i.e. file base names) to their
                    corresponding node indices.

    ----------
    """

@@ -24,47 +33,138 @@ class CandidateGraph(nx.Graph):
        super(CandidateGraph, self).__init__(*args, **kwargs)
        self.node_counter = 0
        node_labels = {}
        self.node_name_map = {}

        # the node_name is the relative path for the image
        for node_name, node_attributes in self.nodes_iter(data=True):

            if os.path.isabs(node_name):
                node_attributes['image_name'] = os.path.basename(node_name)
                node_attributes['image_path'] = node_name
            else:
                node_attributes['image_name'] = node_name
                node_attributes['image_path'] = None
                node_attributes['image_name'] = os.path.basename(os.path.abspath(node_name))
                node_attributes['image_path'] = os.path.abspath(node_name)

            node_labels[node_attributes['image_name']] = self.node_counter
            # fill the dictionary used for relabelling nodes with relative path keys
            node_labels[node_name] = self.node_counter
            # fill the dictionary used for mapping base name to node index
            self.node_name_map[node_attributes['image_name']] = self.node_counter
            self.node_counter += 1

        nx.relabel_nodes(self, node_labels, copy=False)

    @classmethod
    def from_adjacency_file(cls, inputfile):
        """
        Instantiate the class using an adjacency file. This file must contain relative or
        absolute paths to image files.

        Parameters
        ----------
        inputfile : str
                    The input file containing the graph representation

        Returns
        -------
         : object
           A Network graph object

        Examples
        --------
        >>> from autocnet.examples import get_path
        >>> inputfile = get_path('adjacency.json')
        >>> candidate_graph = network.CandidateGraph.from_adjacency_file(inputfile)
        """
        adjacency_dict = io_json.read_json(inputfile)
        return cls(adjacency_dict)

    def get_name(self, nodeIndex):
        """
        Get the image name for the given node.

        Parameters
        ----------
        nodeIndex : int
                    The index of the node.
        
        Returns
        -------
         : str
           The name of the image attached to the given node.


        """
        return self.node[nodeIndex]['image_name']

    def get_keypoints(self, nodeIndex):
        """
        Get the list of keypoints for the given node.
        
        Parameters
        ----------
        nodeIndex : int
                    The index of the node.
        
        Returns
        -------
         : list
           The list of keypoints for the given node.
        
        """
        return self.node[nodeIndex]['keypoints']

    def add_image(self, *args, **kwargs):
        """
        Adds an image node to the graph.

        Parameters
        ==========
        ----------

        """

        raise NotImplementedError
        self.add_node(self.node_counter, *args, **kwargs)
        #self.node_labels[self.node[self.node_counter]['image_name']] = self.node_counter
        self.node_counter += 1

    def adjacency_to_json(self, outputfile):
    def get_geodataset(self, nodeIndex):
        """
        Write the edge structure to a JSON adjacency list
        Constructs a GeoDataset object from the given node image and assigns the 
        dataset and its NumPy array to the 'handle' and 'image' node attributes.

        Parameters
        ==========
        ----------
        nodeIndex : int
                    The index of the node.

        outputfile : str
                     PATH where the JSON will be written
        """
        adjacency_dict = {}
        for n in self.nodes():
            adjacency_dict[n] = self.neighbors(n)
        io_json.write_json(adjacency_dict, outputfile)
        self.node[nodeIndex]['handle'] = GeoDataset(self.node[nodeIndex]['image_path'])
        self.node[nodeIndex]['image'] = bytescale(self.node[nodeIndex]['handle'].read_array())

    def add_matches(self, matches):
    def extract_features(self, nfeatures) :
        """
        Extracts features from each image in the graph and uses the result to assign the
        node attributes for 'handle', 'image', 'keypoints', and 'descriptors'.

        Parameters
        ----------
        nfeatures : int
                    The number of features to be extracted.

        """
        # Loop through the nodes (i.e. images) on the graph and fill in their attributes.
        # These attributes are...
        #      geo dataset (handle and image)
        #      features (keypoints and descriptors)
        for node, attributes in self.nodes_iter(data=True):
        
            self.get_geodataset(node)
            extraction_params = {'nfeatures' : nfeatures}
            attributes['keypoints'], attributes['descriptors'] = fe.extract_features(attributes['image'], 
                                                                                     extraction_params)

    def add_matches(self, matches):
        """
        Adds match data to a node and attributes the data to the
        appropriate edges, e.g. if A-B have a match, edge A-B is attributes
        with the pandas dataframe.
@@ -199,28 +299,19 @@ class CandidateGraph(nx.Graph):

        return merged_cnet

    @classmethod
    def from_adjacency(cls, inputfile):
    def to_json_file(self, outputfile):
        """
        Instantiate the class using an adjacency list
        Write the edge structure to a JSON adjacency list

        Parameters
        ----------
        inputfile : str
                    The input file containing the graph representation

        Returns
        -------
         : object
           A Network graph object
        ==========

        Examples
        --------
        >>> from autocnet.examples import get_path
        >>> inputfile = get_path('adjacency.json')
        >>> candidate_graph = network.CandidateGraph.from_adjacency(inputfile)
        outputfile : str
                     PATH where the JSON will be written
        """
        adjacency_dict = io_json.read_json(inputfile)
        return cls(adjacency_dict)
        adjacency_dict = {}
        for n in self.nodes():
            adjacency_dict[n] = self.neighbors(n)
        io_json.write_json(adjacency_dict, outputfile)

+30 −10
Original line number Diff line number Diff line
import os
import sys
sys.path.insert(0, os.path.abspath('..'))

import cv2
import numpy as np
import unittest

from autocnet.examples import get_path

import sys
sys.path.insert(0, os.path.abspath('..'))

from .. import network


class TestCandidateGraph(unittest.TestCase):
    
    def setUp(self):
        self.graph = network.CandidateGraph.from_adjacency(get_path('adjacency.json'))
        self.graph = network.CandidateGraph.from_adjacency_file(get_path('adjacency.json'))

    def test_get_name(self):
        node_number = self.graph.node_name_map['AS15-M-0297_SML.png']
        name = self.graph.get_name(node_number)
        self.assertEquals(name, 'AS15-M-0297_SML.png')

    def test_add_image(self):
        with self.assertRaises(NotImplementedError):
            self.graph.add_image()
        self.assertEqual(self.graph.node_counter, 7)

    def test_adjacency_to_json(self):
        self.graph.adjacency_to_json('test_adjacency_to_json.json')
        self.assertTrue(os.path.exists('test_adjacency_to_json.json'))
    def test_to_json_file(self):
        self.graph.to_json_file('test_graph_to_json.json')
        self.assertTrue(os.path.exists('test_graph_to_json.json'))

    def test_extract_features(self):
        # also tests get_geodataset() and get_keypoints
        self.graph.extract_features(10)
        node_number = self.graph.node_name_map['AS15-M-0297_SML.png']
        node = self.graph.node[node_number]
        self.assertEquals(len(node['image']), 1012)
        self.assertEquals(len(node['keypoints']), 10)
        self.assertEquals(len(node['descriptors']), 10)
        self.assertIsInstance(node['keypoints'][0], type(cv2.KeyPoint()))
        self.assertIsInstance(node['descriptors'][0], np.ndarray)
        self.assertEquals(self.graph.get_keypoints(node_number), node['keypoints'])


    def tearDown(self):
        try:
            os.remove('test_adjacency_to_json.json')
            os.remove('test_graph_to_json.json')
        except:
            pass
+5 −17
Original line number Diff line number Diff line
import os
import unittest

import pandas as pd
from scipy.misc import bytescale

from autocnet.examples import get_path
from autocnet.fileio.io_gdal import GeoDataset
from autocnet.fileio.io_controlnetwork import to_isis
from autocnet.fileio.io_gdal import GeoDataset
from autocnet.graph.network import CandidateGraph
from autocnet.matcher import feature_extractor as fe
from autocnet.matcher.matcher import FlannMatcher
@@ -42,20 +43,13 @@ class TestTwoImageMatching(unittest.TestCase):
        # Step: Create an adjacency graph
        adjacency = get_path('two_image_adjacency.json')
        basepath = os.path.dirname(adjacency)
        cg = CandidateGraph.from_adjacency(adjacency)
        cg = CandidateGraph.from_adjacency_file(adjacency)
        self.assertEqual(2, cg.number_of_nodes())
        self.assertEqual(1, cg.number_of_edges())

        # Step: Extract image data and attribute nodes
        cg.extract_features(25)
        for node, attributes in cg.nodes_iter(data=True):
            dataset = GeoDataset(os.path.join(basepath, attributes['image_name']))
            attributes['handle'] = dataset
            img = bytescale(dataset.read_array())
            attributes['image'] = img

            # Step: Then find features and descriptors
            attributes['keypoints'], attributes['descriptors'] = fe.extract_features(attributes['image'],
                                                                                     {'nfeatures':25})
            self.assertIn(len(attributes['keypoints']), [24, 25, 26])

        # Step: Then apply a FLANN matcher
@@ -66,15 +60,9 @@ class TestTwoImageMatching(unittest.TestCase):

        for node, attributes in cg.nodes_iter(data=True):
            descriptors = attributes['descriptors']
            matches = fl.query(descriptors, node,  k=3)
            matches = fl.query(descriptors, node,  k=2)
            cg.add_matches(matches)

        # Step: Compute Homography
        transformation_matrix, mask = cg.compute_homography(0, 1)
        self.assertEquals(len(transformation_matrix), 3)
        #TODO: write better test
        #self.assertEquals(len(mask), 19)

        # Step: And create a C object
        cnet = cg.to_cnet()