Commit 2f9ec26d authored by Jay's avatar Jay
Browse files

Updates to make transformations stateless and add binary save/load functionality

parent 0bef87c5
Loading
Loading
Loading
Loading
+1 −0
Changes for autocnet/__init__.py: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -52,4 +52,5 @@ def cuda(enable=False, gpu=0):

    from autocnet.matcher.cpu_decompose import decompose_and_match
    Edge.decompose_and_match = decompose_and_match

cuda()
+33 −268
Changes for autocnet/graph/edge.py: 33 added lines, 268 removed lines.
Original line number Diff line number Diff line
@@ -12,7 +12,8 @@ from autocnet.matcher import health
from autocnet.matcher import outlier_detector as od
from autocnet.matcher import suppression_funcs as spf
from autocnet.matcher import subpixel as sp
from autocnet.transformation.transformations import FundamentalMatrix, Homography
from autocnet.transformation import fundamental_matrix as fm
from autocnet.transformation import homography as hm
from autocnet.vis.graph_view import plot_edge, plot_node, plot_edge_decomposition
from autocnet.cg import cg

@@ -38,8 +39,8 @@ class Edge(dict, MutableMapping):
    def __init__(self, source=None, destination=None):
        self.source = source
        self.destination = destination
        self.homography = None
        self.fundamental_matrix = None
        self['homography'] = None
        self['fundamental_matrix'] = None
        self.matches = None
        self['weight'] = {}

@@ -50,15 +51,20 @@ class Edge(dict, MutableMapping):
        Available Masks: {}
        """.format(self.source, self.destination, self.masks)

    def __getitem__(self, item):
        attribute_dict = {'source': self.source,
                          'destination': self.destination,
                          'masks': self.masks,
                          'weight': self['weight']}
        if item in attribute_dict.keys():
            return attribute_dict[item]
        else:
            return super(Edge, self).__getitem__(item)
    def __eq__(self, other):
        eq = True
        d = self.__dict__
        o = other.__dict__
        for k, v in d.items():
            if isinstance(v, pd.DataFrame):
                if not v.equals(o[k]):
                    print('E', k, self.source['node_id'], self.destination['node_id'])
                    eq = False
            elif isinstance(v, np.ndarray):
                if not v.all() == o[k].all():
                    eq = False
                    print('E2', k)
        return eq

    @property
    def masks(self):
@@ -88,247 +94,6 @@ class Edge(dict, MutableMapping):
        boolean_mask = v[1]
        self.masks[column_name] = boolean_mask

    def decompose_and_match(self, k=2, maxiteration=3, size=18, buf_dist=3,**kwargs):
        """
        Similar to match, this method first decomposed the image into
        $4^{maxiteration}$ subimages and applys matching between each sub-image.

        This method is potential slower than the standard match due to the
        overhead in matching, but can be significantly more accurate.  The
        increase in accuracy is a function of the total image size.  Suggested
        values for maxiteration are provided below.

        Parameters
        ----------
        k : int
            The number of neighbors to find

        method : {'coupled', 'whole'}
                 whether to utilize coupled decomposition
                 or match the whole image

        maxiteration : int
                       When using coupled decomposition, the number of recursive
                       divisions to apply.  The total number of resultant
                       sub-images will be 4 ** maxiteration.  Approximate values:

                        | Number of megapixels | maxiteration |
                        |----------------------|--------------|
                        | m < 10               |1-2|
                        | 10 < m < 30          | 3 |
                        | 30 < m < 100         | 4 |
                        | 100 < m < 1000       | 5 |
                        | m > 1000             | 6 |

        size : int
               When using coupled decomposition, the total number of points
               to check in each sub-image to try and find a match.
               Selection of this number is a balance between seeking a
               representative mid-point and computational cost.

        buf_dist : int
                   When using coupled decomposition, the distance from the edge of
                   the (sub)image a point must be in order to be used as a
                   partioning point.  The smaller the distance, the more likely
                   percision errors can results in erroneous partitions.
        """
        def mono_matches(a, b, aidx=None, bidx=None):
            """
            Apply the FLANN match_features

            Parameters
            ----------
            a : object
                A node object

            b : object
                A node object

            aidx : iterable
                   An index for the descriptors to subset

            bidx : iterable
                   An index for the descriptors to subset
            """
            # Subset if requested
            if aidx is not None:
                ad = a.descriptors[aidx]
            else:
                ad = a.descriptors

            if bidx is not None:
                bd = b.descriptors[bidx]
            else:
                bd = b.descriptors

            # Load, train, and match
            fl.add(ad, a['node_id'], index=aidx)
            fl.train()
            matches = fl.query(bd, b['node_id'], k, index=bidx)
            self._add_matches(matches)
            fl.clear()

        def func(group):
            ratio = 0.8
            res = [False] * len(group)
            if len(res) == 1:
                return [single]
            if group.iloc[0] < group.iloc[1] * ratio:
                res[0] = True
            return res

        # Grab the original image arrays
        sdata = self.source.get_array()
        ddata = self.destination.get_array()

        ssize = sdata.shape
        dsize = ddata.shape

        # Grab all the available candidate keypoints
        skp = self.source.get_keypoints()
        dkp = self.destination.get_keypoints()

        # Set up the membership arrays
        self.smembership = np.zeros(sdata.shape, dtype=np.int16)
        self.dmembership = np.zeros(ddata.shape, dtype=np.int16)
        self.smembership[:] = -1
        self.dmembership[:] = -1
        pcounter = 0

        # FLANN Matcher
        fl= FlannMatcher()

        for k in range(maxiteration):
            partitions = np.unique(self.smembership)
            for p in partitions:
                sy_part, sx_part = np.where(self.smembership == p)
                dy_part, dx_part = np.where(self.dmembership == p)

                # Get the source extent
                minsy = np.min(sy_part)
                maxsy = np.max(sy_part) + 1
                minsx = np.min(sx_part)
                maxsx = np.max(sx_part) + 1

                # Get the destination extent
                mindy = np.min(dy_part)
                maxdy = np.max(dy_part) + 1
                mindx = np.min(dx_part)
                maxdx = np.max(dx_part) + 1

                # Clip the sub image from the full images
                asub = sdata[minsy:maxsy, minsx:maxsx]
                bsub = ddata[mindy:maxdy, mindx:maxdx]

                # Utilize the FLANN matcher to find a match to approximate a center
                fl.add(self.destination.descriptors, self.destination['node_id'])
                fl.train()

                scounter = 0
                decompose = False
                while True:
                    sub_skp = skp.query('x >= {} and x <= {} and y >= {} and y <= {}'.format(minsx, maxsx, minsy, maxsy))
                    # Check the size to ensure a valid return
                    if len(sub_skp) == 0:
                        break # No valid keypoints in this (sub)image
                    if size > len(sub_skp):
                        size = len(sub_skp)
                    candidate_idx = np.random.choice(sub_skp.index, size=size, replace=False)
                    candidates = self.source.descriptors[candidate_idx]
                    matches = fl.query(candidates, self.source['node_id'], k=3, index=candidate_idx)

                    # Apply Lowe's ratio test to try to find a 'good' starting point
                    mask = matches.groupby('source_idx')['distance'].transform(func).astype('bool')
                    candidate_matches = matches[mask]
                    match_idx = candidate_matches['source_idx']

                    # Extract those matches that pass the ratio check
                    sub_skp = skp.iloc[match_idx]

                    # Check that valid points remain
                    if len(sub_skp) == 0:
                        break

                    # Locate the candidate closest to the middle of all of the matches
                    smx, smy = sub_skp[['x', 'y']].mean()
                    mid = np.array([[smx, smy]])
                    dists = cdist(mid, sub_skp[['x', 'y']])
                    closest = sub_skp.iloc[np.argmin(dists)]
                    closest_idx = closest.name
                    soriginx, soriginy = closest[['x', 'y']]

                    # Grab the corresponding point in the destination
                    q = candidate_matches.query('source_idx == {}'.format(closest.name))
                    dest_idx = q['destination_idx'].iat[0]
                    doriginx = dkp.at[dest_idx, 'x']
                    doriginy = dkp.at[dest_idx, 'y']

                    if mindy + buf_dist <= doriginy <= maxdy - buf_dist\
                     and mindx + 3 <= doriginx <= maxdx - 3:
                        # Point is good to split on
                        decompose = True
                        break
                    else:
                        scounter += 1
                        if scounter >= maxiteration:
                            break

                # Clear the Flann matcher for reuse
                fl.clear()

                # Check that the identified match falls within the (sub)image
                # This catches most bad matches that have passed the ratio check
                if not (buf_dist <= doriginx - mindx <= bsub.shape[1] - buf_dist) or not\
                       (buf_dist <= doriginy - mindy <= bsub.shape[0] - buf_dist):
                       decompose = False

                if decompose:
                    # Apply coupled decomposition, shifting the origin to the sub-image
                    s_submembership, d_submembership = coupled_decomposition(asub, bsub,
                                                                         sorigin=(soriginx - minsx, soriginy - minsy),
                                                                         dorigin=(doriginx - mindx, doriginy - mindy),
                                                                         **kwargs)

                    # Shift the returned membership counters to a set of unique numbers
                    s_submembership += pcounter
                    d_submembership += pcounter

                    # And assign membership
                    self.smembership[minsy:maxsy,
                                minsx:maxsx] = s_submembership
                    self.dmembership[mindy:maxdy,
                                mindx:maxdx] = d_submembership
                    pcounter += 4

        # Now match the decomposed segments to one another
        for p in np.unique(self.smembership):
            sy_part, sx_part = np.where(self.smembership == p)
            dy_part, dx_part = np.where(self.dmembership == p)

            # Get the source extent
            minsy = np.min(sy_part)
            maxsy = np.max(sy_part) + 1
            minsx = np.min(sx_part)
            maxsx = np.max(sx_part) + 1

            # Get the destination extent
            mindy = np.min(dy_part)
            maxdy = np.max(dy_part) + 1
            mindx = np.min(dx_part)
            maxdx = np.max(dx_part) + 1

            # Get the indices of the candidate keypoints within those regions / variables are pulled before decomp.
            sidx = skp.query('x >= {} and x <= {} and y >= {} and y <= {}'.format(minsx, maxsx, minsy, maxsy)).index
            didx = dkp.query('x >= {} and x <= {} and y >= {} and y <= {}'.format(mindx, maxdx, mindy, maxdy)).index
            # If the candidates < k, OpenCV throws an error
            if len(sidx) >= k and len(didx) >=k:
                mono_matches(self.source, self.destination, sidx, didx)
                mono_matches(self.destination, self.source, didx, sidx)
=======
    @property
    def health(self):
        return self._health.health

    def decompose_and_match(*args, **kwargs):
        pass

@@ -399,19 +164,15 @@ class Edge(dict, MutableMapping):
        s_keypoints.index = matches.index
        d_keypoints.index = matches.index

        self.fundamental_matrix = FundamentalMatrix(np.zeros((3,3)), index=matches.index)
        self.fundamental_matrix.compute(s_keypoints, d_keypoints, **kwargs)
        self['fundamental_matrix'], fmask = fm.compute_fundamental_matrix(s_keypoints, d_keypoints, **kwargs)

        # Convert the truncated RANSAC mask back into a full length mask
        mask[mask] = self.fundamental_matrix.mask

        # Subscribe the health watcher to the fundamental matrix observable
        self.fundamental_matrix._notify_subscribers(self.fundamental_matrix)
        mask[mask] = fmask

        # Set the initial state of the fundamental mask in the masks
        self.masks = ('fundamental', mask)

    def refine_fundamental_matrix_matches(self, **kwargs): # pragma: no cover
    def refine_fundamental_matrix_matches(self, clean_keys=[], **kwargs): # pragma: no cover
        """
        Given an estimated fundamental matrix, refine the correspondences based
        on the reprojective error.
@@ -423,8 +184,17 @@ class Edge(dict, MutableMapping):
        if not hasattr(self, 'fundamental_matrix'):
            raise AttributeError('No fundamental matrix exists for this edge.')
            return
        # TODO: Homogeneous is horribly inefficient here, use Numpy array notation
        s_keypoints = self.source.get_keypoint_coordinates(index=matches['source_idx'],
                                                                 homogeneous=True)
        d_keypoints = self.destination.get_keypoint_coordinates(index=matches['destination_idx'],
                                                                homogeneous=True)

        self.fundamental_matrix.refine_matches(**kwargs)

        mask = update_fundamental_mask(self['fundamental_matrix'],
                                       s_keypoints, d_keypoints,
                                       index=self.matches.index, **kwargs)
        self.masks = ('fundamental', mask)

    def compute_homography(self, method='ransac', clean_keys=[], pid=None, **kwargs):
        """
@@ -456,17 +226,12 @@ class Edge(dict, MutableMapping):
        s_keypoints = self.source.get_keypoint_coordinates(index=matches['source_idx'])
        d_keypoints = self.destination.get_keypoint_coordinates(index=matches['destination_idx'])

        self.homography = Homography(np.zeros((3,3)), index=self.masks.index)
        self.homography.compute(s_keypoints.values,
                                d_keypoints.values)
        self['homography'], hmask = hm.compute_homography(s_keypoints.values, d_keypoints.values)

        # Convert the truncated RANSAC mask back into a full length mask
        mask[mask] = self.homography.mask
        mask[mask] = hmask
        self.masks = ('ransac', mask)

        # Finalize the array to get custom attrs to propagate
        self.homography.__array_finalize__(self.homography)

    def subpixel_register(self, clean_keys=[], threshold=0.8,
                          template_size=19, search_size=53, max_x_shift=1.0,
                          max_y_shift=1.0, tiled=False, **kwargs):

autocnet/graph/mcl.py

deleted100644 → 0
+0 −0

Empty file deleted.

+14 −8
Changes for autocnet/graph/network.py: 14 added lines, 8 removed lines.
Original line number Diff line number Diff line
@@ -7,8 +7,7 @@ import dill as pickle
import networkx as nx
import pandas as pd

from plio.io import io_hdf
from plio.io import io_json
from plio.io import io_hdf, io_json, io_autocnetgraph
from plio.utils import utils as io_utils
from plio.io.io_gdal import GeoDataset
from autocnet.graph import markov_cluster
@@ -70,6 +69,17 @@ class CandidateGraph(nx.Graph):
        self.graph['creationdate'] = strftime("%Y-%m-%d %H:%M:%S", gmtime())
        self.graph['modifieddate'] = strftime("%Y-%m-%d %H:%M:%S", gmtime())

    def __eq__(self, other):
        eq = True
        # Check the nodes
        for n in self.nodes_iter():
            if not self.node[n] == other.node[n]:
                eq = False
        for s, d in self.edges_iter():
            if not self.edge[s][d] == other.edge[s][d]:
                eq = False
        return eq

    @classmethod
    def from_graph(cls, graph):
        """
@@ -545,12 +555,7 @@ class CandidateGraph(nx.Graph):
        filename : str
                   The relative or absolute PATH where the network is saved
        """
        for i, node in self.nodes_iter(data=True):
            # Close the file handle because pickle doesn't handle SwigPyObjects
            node._handle = None

        with open(filename, 'wb') as f:
            pickle.dump(self, f, protocol=pickle.HIGHEST_PROTOCOL)
        io_autocnetgraph.save(self, filename)

    def plot(self, ax=None, **kwargs):  # pragma: no cover
        """
@@ -670,6 +675,7 @@ class CandidateGraph(nx.Graph):
        bunch = set(self.nbunch_iter(nodes))
        # create new graph and copy subgraph into it
        H = self.__class__()

        # copy node and attribute dictionaries
        for n in bunch:
            H.node[n] = self.node[n]
+26 −10
Changes for autocnet/graph/node.py: 26 added lines, 10 removed lines.
Original line number Diff line number Diff line
@@ -61,10 +61,11 @@ class Node(dict, MutableMapping):
        self['image_name'] = image_name
        self['image_path'] = image_path
        self['node_id'] = node_id
        self['hash'] = self['image_name']  #TODO: Repalce with farmhash
        self['hash'] = image_name
        self._mask_arrays = {}
        self.point_to_correspondence = defaultdict(set)
        self.point_to_correspondence_df = None
        self.descriptors = None

    def __repr__(self):
        return """
@@ -77,10 +78,24 @@ class Node(dict, MutableMapping):
        """.format(self['node_id'], self['image_name'], self['image_path'],
                   self.nkeypoints, self.masks, self.__class__)

    def __eq__(self, other):
        eq = True
        d = self.__dict__
        o = other.__dict__
        for k, v in d.items():
            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):
        attribute_dict = {'image_name': self.image_name,
                          'image_path': self.image_path,
        attribute_dict = {'image_name': self['image_name'],
                          'image_path': self['image_path'],
                          'geodata': self.geodata,
                          'keypoints': self.keypoints,
                          'nkeypoints': self.nkeypoints,
@@ -92,6 +107,7 @@ class Node(dict, MutableMapping):
        else:
            return super(Node, self).__getitem__(item)
    """

    @property
    def geodata(self):
        if not getattr(self, '_geodata', None) and self['image_path'] is not None:
@@ -147,7 +163,7 @@ class Node(dict, MutableMapping):
        else:
            return 0

    @property
    """    @property
    def keypoints(self):
        if hasattr(self, '_keypoints'):
            return self._keypoints.copy()
@@ -159,7 +175,7 @@ class Node(dict, MutableMapping):
        if hasattr(self, '_descriptors'):
            return np.copy(self._descriptors)
        else:
            return None
            return None"""

    def coverage(self):
        """
@@ -294,7 +310,7 @@ class Node(dict, MutableMapping):
        else:
            hdf = in_path

        self._descriptors = hdf['{}/descriptors'.format(self['image_name'])][:]
        self.descriptors = hdf['{}/descriptors'.format(self['image_name'])][:]
        raw_kps = hdf['{}/keypoints'.format(self['image_name'])][:]
        index = raw_kps['index']
        clean_kps = utils.remove_field_name(raw_kps, 'index')
@@ -334,17 +350,17 @@ class Node(dict, MutableMapping):
        else:
            hdf = out_path

        try:
        #try:
        hdf.create_dataset('{}/descriptors'.format(self['image_name']),
                               data=self._descriptors,
                           data=self.descriptors,
                           compression=io_hdf.DEFAULT_COMPRESSION,
                           compression_opts=io_hdf.DEFAULT_COMPRESSION_VALUE)
        hdf.create_dataset('{}/keypoints'.format(self['image_name']),
                           data=hdf.df_to_sarray(self._keypoints.reset_index()),
                           compression=io_hdf.DEFAULT_COMPRESSION,
                           compression_opts=io_hdf.DEFAULT_COMPRESSION_VALUE)
        except:
            warnings.warn('Descriptors for the node {} are already stored'.format(self['image_name']))
        #except:
            #warnings.warn('Descriptors for the node {} are already stored'.format(self['image_name']))

        # If the out_path is a string, assume this method is being called as a singleton
        # and close the hdf file gracefully.  If an object, let the instantiator of the
Loading