Loading .gitignore +11 −3 Original line number Diff line number Diff line Loading @@ -2,6 +2,10 @@ /.ipynb* *.ipynb* # Notebooks dir notebooks/* !notebooks/*.ipynb #PyCharm /.idea Loading Loading @@ -70,6 +74,10 @@ target/ *.swp #Data and output files .csv .png .SAV *.csv *.png *.SAV *.net *.cnet *.lis *.list .travis.yml +1 −1 Original line number Diff line number Diff line Loading @@ -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 Loading autocnet/control/control.py +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 autocnet/control/tests/test_control.py +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])) autocnet/graph/edge.py +2 −55 Original line number Diff line number Diff line Loading @@ -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): Loading Loading @@ -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 Loading
.gitignore +11 −3 Original line number Diff line number Diff line Loading @@ -2,6 +2,10 @@ /.ipynb* *.ipynb* # Notebooks dir notebooks/* !notebooks/*.ipynb #PyCharm /.idea Loading Loading @@ -70,6 +74,10 @@ target/ *.swp #Data and output files .csv .png .SAV *.csv *.png *.SAV *.net *.cnet *.lis *.list
.travis.yml +1 −1 Original line number Diff line number Diff line Loading @@ -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 Loading
autocnet/control/control.py +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
autocnet/control/tests/test_control.py +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]))
autocnet/graph/edge.py +2 −55 Original line number Diff line number Diff line Loading @@ -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): Loading Loading @@ -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