Commit 4fe32abb authored by Kelvin Rodriguez's avatar Kelvin Rodriguez Committed by GitHub
Browse files

Merge pull request #220 from jlaura/control

Control
parents 53d77c2c 322909d8
Loading
Loading
Loading
Loading
+11 −3
Original line number Diff line number Diff line
@@ -2,6 +2,10 @@
/.ipynb*
*.ipynb*

# Notebooks dir
notebooks/*
!notebooks/*.ipynb

#PyCharm
/.idea

@@ -70,6 +74,10 @@ target/
*.swp

#Data and output files
.csv
.png
.SAV
*.csv
*.png
*.SAV
*.net
*.cnet
*.lis
*.list
+1 −1
Original line number Diff line number Diff line
@@ -65,7 +65,7 @@ install:
  - python condaci.py setup

script:
  - pytest --cov=autocnet
  - pytest autocnet tests

after_success:
  # Upload to anaconda and push to coveralls
+162 −144
Original line number Diff line number Diff line
import collections
from time import gmtime, strftime
import networkx as nx
import numpy as np
import pandas as pd
import geopandas as gpd
from shapely.geometry import Point

from plio.io.io_controlnetwork import to_isis, write_filelist

class Point(object):

def identify_potential_overlaps(cg, cn, overlap=True):
    """
    An n-image correspondence container class to store
    information common to all identical correspondences across
    an image set.
    Identify those points that could have additional measures

    Attributes
    Parameters
    ----------
    point_id : int
               A unique identifier for the given point

    subpixel : bool
               Whether or not the point has been subpixel registered

    point_type : an ISIS identifier for the type of the point
                 as defined in the ISIS protobuf spec.

    correspondences : list
                      of image correspondences
    overlap : boolean
              If True, apply aprint(g)n additional point in polygon check, where
              the polygon is the footprint intersection between images and
              the point is a keypoint projected into lat/lon space.  Note
              that the projection can be inaccurate if the method used
              estimates the transformation.

    Returns
    -------
    candidate_cliques : DataFrame
                        with the index as the point id (in the data attribute)
                        and the value as an iterable of image ids to search
                        for a new point.
    """
    __slots__ = '_subpixel', 'point_id', 'point_type', 'correspondences'

    def __init__(self, pid, point_type=2):
        self.point_id = pid
        self._subpixel = False
        self.point_type = point_type
        self.correspondences = []

    def __repr__(self):
        return str(self.point_id)

    def __eq__(self, other):
        return self.point_id == other

    def __hash__(self):
        return hash(self.point_id)

    @property
    def subpixel(self):
        return self._subpixel

    @subpixel.setter
    def subpixel(self, v):
        if isinstance(v, bool):
            self._subpixel = v
        if self._subpixel is True:
            self.point_type = 3
    fc = cg.compute_fully_connected_components()

    candidate_cliques = []
    geoms = []
    idx = []
    for i, p in cn.data.groupby('point_id'):
        # Which images are covered already.  This finds any connected cycles that
        #  a node is in (this can be more than one - an hourglass network for example)
        # Extract the fully connected subgraph for each covered image in order to
        #  identify which subgraph the measure is in
        covered = p['image_index']
        candidate_cycles = [fc[c] for c in covered]
        cycle = [i for i in candidate_cycles if candidate_cycles.count(i) > 1]
        cycle_to_punch = cycle[0][0]

class Correspondence(object):
    """
    A single correspondence (image measure).
        # Using the cycles to punch, which images could also be covered?
        uncovered = tuple(set(cycle_to_punch).difference(set(covered)))

    Attributes
    ----------
        # All candidates are covered, skip this point
        if not uncovered:
            continue

    id : int
         The index of the point in a matches dataframe (stored as an edge attribute)
        # Determine whether a 'real' lat/lon are to be used and reproject
        if overlap:
            row = p.iloc[0]
            lat, lon = cg.node[row.image_index].geodata.pixel_to_latlon(row.x, row.y)
        else:
            lat, lon = 0,0

    x : float
        The x coordinate of the measure in image space
        # Build the data for the geodataframe - can the index be cleaner?
        geoms.append(Point(lon, lat))
        candidate_cliques.append([uncovered, cycle_to_punch])
        idx.append(i)

    y : float
        The y coordinate of the measure in image space

    measure_type : int
                   The ISIS measure type as per the protobuf spec
    candidate_cliques = gpd.GeoDataFrame(candidate_cliques, index=idx,
                                     columns=['candidates', 'subgraph'], geometry=geoms)

    serial : str
             A unique serial number for the image the measure corresponds to
             In the case of an ISIS cube, this is a valid ISIS serial number,
             else, None.
    def overlaps(group):
        """
    __slots__ = 'id', 'x', 'y', 'measure_type', 'serial'

    def __init__(self, id, x, y, measure_type=2, serial=None):
        self.id = id
        self.x = x
        self.y = y
        self.measure_type = measure_type
        self.serial = serial

    def __repr__(self):
        return str(self.id)

    def __eq__(self, other):
        return self.id == other

    def __hash__(self):
        return hash(self.id)
        Take a group, find the subgraph, compute the intersection of footprints
        and apply a group point in polygon check. This is an optimization where
        n-points are intersected with the poly at once (as opposed to the
        single iteration approach.)
        """
        cycle_to_punch = group.subgraph.iloc[0]
        subgraph = cg.create_node_subgraph(cycle_to_punch)
        union, _ = subgraph.compute_intersection(cycle_to_punch[0])#.query('overlaps_all == True')
        intersection = group.intersects(union.unary_union)
        return intersection

    # If the overlap check is going to be used, apply it.
    if overlap:
        candidate_cliques['overlap'] = False
        for i, g in candidate_cliques.groupby('candidates'):
            intersection = overlaps(g)
            candidate_cliques.loc[intersection.index, 'overlap'] = intersection
        return candidate_cliques.query('overlap == True')['candidates']
    else:
         return candidate_cliques.candidates

def deepen_correspondences(cg, cn):
    pass

class ControlNetwork(object):
    measures_keys = ['point_id', 'image_index', 'keypoint_index', 'edge', 'match_idx', 'x', 'y']

class CorrespondenceNetwork(object):
    def __init__(self):
        self._point_id = 0
        self._measure_id = 0
        self.measure_to_point = {}
        self.data = pd.DataFrame(columns=self.measures_keys)

    @classmethod
    def from_candidategraph(cls, matches):
        cls = ControlNetwork()
        for match in matches:
            for idx, row in match.iterrows():
                edge = (row.source_image, row.destination_image)
                source_key = (row.source_image, row.source_idx)
                source_fields = row[['source_x', 'source_y']]
                destin_key = (row.destination_image, row.destination_idx)
                destin_fields = row[['destination_x', 'destination_y']]
                if cls.measure_to_point.get(source_key, None) is not None:
                    tempid = cls.measure_to_point[source_key]
                    cls.add_measure(destin_key, edge, row.name, destin_fields, point_id=tempid)
                elif cls.measure_to_point.get(destin_key, None) is not None:
                    tempid = cls.measure_to_point[destin_key]
                    cls.add_measure(source_key, edge, row.name,  source_fields, point_id=tempid)
                else:
                    cls.add_measure(source_key, edge, row.name,  source_fields)
                    cls.add_measure(destin_key, edge,row.name,  destin_fields)
                    cls._point_id += 1

        cls.data.index.name = 'measure_id'
        return cls

    def add_measure(self, key, edge, match_idx, fields, point_id=None):
        """
    A container of points and associated correspondences.  The primary
    data structures are point_to_correspondence and correspondence_to_point.
    These two attributes store the mapping between point and correspondences.
        Create a new measure that is coincident to a given point.  This method does not
        create the point if is missing.  When a measure is added to the graph, an associated
        row is added to the measures dataframe.

    Attributes
        Parameters
        ----------
    point_to_correspondence : dict
                              with key equal to an instance of the Point class and
                              values equal to a list of Correspondences.

    correspondence_to_point : dict
                              with key equal to a correspondence identifier (not the class) and
                              value equal to a unique point_id (not an instance of the Point class).
                              This attribute serves as a low memory reverse lookup table

    point_id : int
               The current 'new' point id if an additional point were to be added

    n_points : int
               The number of points in the CorrespondenceNetwork
        key : hashable
                  Some hashable id.  In the case of an autocnet graph object the
                  id should be in the form (image_id, match_id)

    n_measures : int
                 The number of Correspondences in the CorrespondenceNetwork

    creationdate : str
                   The date the instance of this class was first instantiated

    modifieddata : str
                   The date this class last had correspondences and/or points added
        point_id : hashable
                   The point to link the node to.  This is most likely an integer, but
                   any hashable should work.
        """
    def __init__(self):
        self.point_to_correspondence = collections.defaultdict(list)
        self.correspondence_to_point = {}
        self.point_id = 0
        self.creationdate = strftime("%Y-%m-%d %H:%M:%S", gmtime())
        self.modifieddate = strftime("%Y-%m-%d %H:%M:%S", gmtime())

    @property
    def n_points(self):
        return len(self.point_to_correspondence.keys())

    @property
    def n_measures(self):
        return len(self.correspondence_to_point.keys())

    def add_correspondences(self, edge, matches):
        # Convert the matches dataframe to a dict
        df = matches.to_dict()
        source_image = next(iter(df['source_image'].values()))
        destination_image = next(iter(df['destination_image'].values()))
        if key in self.measure_to_point.keys():
            return
        if point_id == None:
            point_id = self._point_id
        self.measure_to_point[key] = point_id
        # The node_id is a composite key (image_id, correspondence_id), so just grab the image
        image_id = key[0]
        match_id = key[1]
        self.data.loc[self._measure_id] = [point_id, image_id, match_id, edge, match_idx, *fields]
        self._measure_id += 1

    def validate_points(self):
        """
        Ensure that all control points currently in the nework are valid.

        # TODO: Handle subpixel registration here
        s_kps = edge.source.get_keypoint_coordinates().values
        d_kps = edge.destination.get_keypoint_coordinates().values
        Criteria for validity:

        # Load the correspondence to point data structure
        for k, source_idx in df['source_idx'].items():
            source_idx = int(source_idx)
            p = Point(self.point_id)
          * Singularity: A control point can have one and only one measure from any image

            destination_idx = int(df['destination_idx'][k])
        Returns
        -------
         : pd.Series

            sidx = Correspondence(source_idx, *s_kps[int(source_idx)], serial=edge.source.isis_serial)
            didx = Correspondence(destination_idx, *d_kps[int(destination_idx)], serial=edge.destination.isis_serial)
        """

            p.correspondences = [sidx, didx]
        def func(g):
            print(g)
            # One and only one measure constraint
            if not g.image_index.duplicated().any():
                return True
            else: return False

            self.correspondence_to_point[(source_image, source_idx)] = self.point_id
            self.correspondence_to_point[(destination_image, destination_idx)] = self.point_id
        return self.data.groupby('point_id').apply(func)

            self.point_to_correspondence[p].append((source_image, sidx))
            self.point_to_correspondence[p].append((destination_image, didx))
    def to_isis(self, outname, serials, olist, *args, **kwargs): #pragma: no cover
        """
        Write the control network out to the ISIS3 control network format.
        """

            self.point_id += 1
        self._update_modified_date()
        if self.validate_points().any() == True:
            warnings.warn('Control Network is not ISIS3 compliant.  Please run the validate_points method on the control network.')
            return

    def _update_modified_date(self):
        self.modifieddate = strftime("%Y-%m-%d %H:%M:%S", gmtime())
        to_isis(outname + '.net', self.data, serials, *args, **kwargs)
        write_filelist(olist, outname + '.lis')

    def to_dataframe(self):
    def to_bal(self):
        """
        Write the control network out to the Bundle Adjustment in the Large
        (BAL) file format.  For more information see:
        http://grail.cs.washington.edu/projects/bal/
        """
        pass
+65 −94
Original line number Diff line number Diff line
import os
import sys
from time import gmtime, strftime
import unittest

from unittest.mock import Mock, MagicMock

from autocnet.graph.edge import Edge
from autocnet.graph.node import Node

import numpy as np
from unittest.mock import MagicMock
import geopandas as gpd
import pandas as pd

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

from autocnet.control import control


class TestC(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        npts = 10
        coords = pd.DataFrame(np.arange(npts * 2).reshape(-1, 2))
        source = np.zeros(npts)
        destination = np.ones(npts)
        pid = np.arange(npts)

        matches = pd.DataFrame(np.vstack((source, pid, destination, pid)).T, columns=['source_image',
                                                                                      'source_idx',
                                                                                      'destination_image',
                                                                                      'destination_idx'])

        edge = Mock(spec=Edge)
        edge.source = Mock(spec=Node)
        edge.destination = Mock(spec=Node)
        edge.source.isis_serial = None
        edge.destination.isis_serial = None
        edge.source.get_keypoint_coordinates = MagicMock(return_value=coords)
        edge.destination.get_keypoint_coordinates = MagicMock(return_value=coords)

        cls.C = control.CorrespondenceNetwork()
        cls.C.add_correspondences(edge, matches)
from shapely.geometry import Polygon


    def test_n_point(self):
        self.assertEqual(self.C.n_points, 10)

    def test_n_measures(self):
        self.assertEqual(self.C.n_measures, 20)

    def test_modified_date(self):
        self.assertIsInstance(self.C.modifieddate, str)

    def test_creation_date(self):
        self.assertEqual(self.C.creationdate, strftime("%Y-%m-%d %H:%M:%S", gmtime()))

    def test_point_subpixel(self):
        for k, v in self.C.point_to_correspondence.items():
            self.assertFalse(k.subpixel)
            k.subpixel = True
            self.assertTrue(k.subpixel)
            break

    def test_equalities(self):
        points = []
        correspondences = []
        for k, v in self.C.point_to_correspondence.items():
            points.append(k)
            correspondences.extend(v)
        self.assertEqual(points[0], points[0])
        self.assertNotEqual(points[-1], points[1])
        self.assertEqual(correspondences[1][0], correspondences[1][0])

    def test_to_dataframe(self):
        self.C.to_dataframe()

    def test_point_repr(self):
        expected = 0
        p = control.Point(expected)
        self.assertEqual(str(expected), p.__repr__())

    def test_correspondence_repr(self):
        expected = 0
        c = control.Correspondence(expected, 1, 1)
        self.assertEqual(str(expected), c.__repr__())

    def test_correspondence_eq(self):
        expected = 0
        c = control.Correspondence(expected, 1, 1)
        self.assertTrue(c == expected)

    def test_correspondence_hash(self):
        expected = 200
        c = control.Correspondence(expected, 1, 1)
        self.assertEqual(hash(expected), hash(c))
import os
import sys
sys.path.insert(0, '..')
from .. import control

def test_fromcandidategraph(candidategraph, controlnetwork_data):#, controlnetwork):
    matches = candidategraph.get_matches()
    cn = control.ControlNetwork.from_candidategraph(matches)
    assert cn.data.equals(controlnetwork_data)

def test_add_measure():
    cn = control.ControlNetwork()

    # Add the point 0 from image 0
    key = (0,0)
    cn.add_measure(key, (0,1), 3, [1,1])
    assert key in cn.measure_to_point.keys()
    assert cn.measure_to_point[key] == 0

    # Add the point 1 from image 2
    key = (1,2)
    cn.add_measure(key, (0,1), 2, [1,1])
    assert key in cn.measure_to_point.keys()

    # Add another measure associated with point (0,0)
    #  Key is the source and this methods is called to add the destination
    key = (2,1)
    cn.add_measure(key, (0,2), 3, [1,1], point_id = 0)
    assert key in cn.measure_to_point.keys()
    assert cn.measure_to_point[key] == 0

def test_validate_points(controlnetwork):
    assert controlnetwork.validate_points().any()

def test_bad_validate_points(bad_controlnetwork):
    assert bad_controlnetwork.validate_points().iloc[0] == False
    assert bad_controlnetwork.validate_points().iloc[1:].all()

def test_identify_potential_overlaps(controlnetwork, candidategraph):
    res = control.identify_potential_overlaps(candidategraph,
                                              controlnetwork,
                                              overlap=False)

    assert res.equals(pd.Series([(2,), (2,),
                                 (1,), (1,),
                                 (0,), (0,)],
                                 index=[6,7,8,9,10,11]))

def test_potential_overlap(controlnetwork, candidategraph):
    # Patch in an intersection check so that all points intersect all geoms
    candidategraph.create_node_subgraph = MagicMock(return_value=candidategraph)
    coords = [(-1., -1.), (-1., 1.), (1., 1.), (1., -1.), (-1., -1.)]
    poly = gpd.GeoSeries(Polygon(coords))
    candidategraph.compute_intersection = MagicMock(return_value=(poly, 0))
    res = control.identify_potential_overlaps(candidategraph,
                                              controlnetwork,
                                              overlap=True)

    assert res.equals(pd.Series([(2,), (2,),
                                 (1,), (1,),
                                 (0,), (0,)],
                                 index=[6,7,8,9,10,11]))
+2 −55
Original line number Diff line number Diff line
@@ -59,40 +59,8 @@ class Edge(dict, MutableMapping):
        """.format(self.source, self.destination, self.masks)

    def __eq__(self, other):
        eq = True
        d = self.__dict__
        o = other.__dict__
        for k, v in d.items():
            # If the attribute key is missing they can not be equal
            if not k in o.keys():
                eq = False
                return eq

            if isinstance(v, pd.DataFrame):
                if not v.equals(o[k]):
                    eq = False
            elif isinstance(v, np.ndarray):
                if not v.all() == o[k].all():
                    eq = False

        return eq

    """@property
    def masks(self):
        mask_lookup = {'fundamental': 'fundamental_matrix'}
        if not hasattr(self, '_masks'):
            if isinstance(self.matches, pd.DataFrame):
                self._masks = pd.DataFrame(True, columns=['symmetry'],
                                           index=self.matches.index)
            else:
                self._masks = pd.DataFrame()
        return self._masks

    @masks.setter
    def masks(self, v):
        column_name = v[0]
        boolean_mask = v[1]
        self.masks[column_name] = boolean_mask"""
        return utils.compare_dicts(self.__dict__, other.__dict__) *\
               utils.compare_dicts(self, other)

    def match(self, k=2, **kwargs):

@@ -136,27 +104,6 @@ class Edge(dict, MutableMapping):
    def decompose_and_match(*args, **kwargs):
        pass

    """
    def extract_subset(self, *args, **kwargs):
        self.compute_overlap()

        # Extract the source
        minx, maxx, miny, maxy = self['source_mbr']
        xystart = (minx, miny)
        pixels=[minx, miny, maxx-minx, maxy-miny]
        node = self.source
        arr = node.geodata.read_array(pixels=pixels)
        node.extract_features(arr, xystart=xystart, *args, **kwargs)

        # Extract the destination
        minx, maxx, miny, maxy = self['destin_mbr']
        xystart = (minx, miny)
        pixels=[minx, miny, maxx-minx, maxy-miny]
        node = self.destination
        arr = node.geodata.read_array(pixels=pixels)
        node.extract_features(arr, xystart=xystart, *args, **kwargs)
    """

    def overlap_check(self):
        """Creates a mask for matches on the overlap"""
        if not (self["source_mbr"] and self["destin_mbr"]):
Loading