Commit afadd7b8 authored by jay's avatar jay
Browse files

Updates to the node and tests for attr changes

parent 3f3583c2
Loading
Loading
Loading
Loading
+70 −16
Changes for autocnet/graph/network.py: 70 added lines, 16 removed lines.
Original line number Diff line number Diff line
@@ -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):
    """
@@ -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):
        """
@@ -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):
        """
@@ -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
@@ -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)

@@ -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)

@@ -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)

@@ -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)

@@ -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)

+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

@@ -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
@@ -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

@@ -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 """
@@ -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):
@@ -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.
@@ -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):
        """
@@ -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):
        """
@@ -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):
        """
@@ -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
@@ -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

@@ -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'):
        """
@@ -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
@@ -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):
        """
@@ -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
@@ -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=[]):
        """
@@ -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
@@ -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
+82 −49
Changes for autocnet/graph/tests/test_node.py: 82 added lines, 49 removed lines.
Original line number Diff line number Diff line
@@ -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
@@ -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)