Commit 2442512f authored by jlaura's avatar jlaura Committed by Kelvin Rodriguez
Browse files

API changes to support server refactor (#255)

* API changes to support server refactor

* Fixes pandas deprecation for indexing

* Updates and integrates ring matcher

* adds subpixel register and costs df

* subpixel tests and fundamental matrices

* Updates tests

* Adds minimal ring_match test
parent 7b19d167
Loading
Loading
Loading
Loading
+223 −99
Original line number Diff line number Diff line
@@ -14,12 +14,14 @@ from autocnet.utils import utils
from autocnet.matcher import cpu_outlier_detector as od
from autocnet.matcher import suppression_funcs as spf
from autocnet.matcher import subpixel as sp
from autocnet.matcher import cpu_ring_matcher
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

from plio.io.io_gdal import GeoDataset
from plio.spatial.transformations import reproject


class Edge(dict, MutableMapping):
@@ -75,9 +77,35 @@ class Edge(dict, MutableMapping):
    def matches(self, value):
        if isinstance(value, pd.DataFrame):
            self._matches = value
            # Ensure that the costs df remains in sync with the matches df
            if not self.costs.index.equals(value.index):
                self.costs = pd.DataFrame(index=value.index)
        else:
            raise(TypeError)
    
    @property
    def costs(self):
        if not hasattr(self, '_costs'):
            self._costs = pd.DataFrame(index=self.matches.index)
        return self._costs

    @costs.setter
    def costs(self, value):
        if isinstance(value, pd.DataFrame):
            self._costs = value
        else:
            raise(TypeError)

    @property
    def ring(self):
        if not hasattr(self, '_ring'):
            self._ring = None
        return self._ring

    @ring.setter
    def ring(self, val):
        self._ring = val

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

        """
@@ -114,6 +142,92 @@ class Edge(dict, MutableMapping):
        """
        pass

    def ring_match(self, *args, **kwargs):
        ref_kps =  self.source.keypoints
        ref_desc = self.source.descriptors
        tar_kps = self.destination.keypoints
        tar_desc = self.destination.descriptors

        if not 'xm' in ref_kps.columns:
            warnings.warn('To ring match body centered coordinates (xm, ym, zm) must be in the keypoints')
            return
        ref_feats = ref_kps[['x', 'y', 'xm', 'ym', 'zm']].values
        tar_feats = tar_kps[['x', 'y', 'xm', 'ym', 'zm']].values

        _, _, pidx, ring = cpu_ring_matcher.ring_match(ref_feats, tar_feats,
                                                           ref_desc, tar_desc,
                                                           *args, **kwargs)

        if pidx is None:
            return
        self.ring = ring
        pidx = cpu_ring_matcher.check_pidx_duplicates(pidx)

        #Set the columns of the matches df
        matches = np.empty((pidx.shape[0], 4))
        matches[:,0] = self.source['node_id']
        matches[:,1] = ref_kps.index[pidx[:,0]].values
        matches[:,2] = self.destination['node_id']
        matches[:,3] = tar_kps.index[pidx[:,1]].values

        matches = pd.DataFrame(matches, columns=['source',
                                                 'source_idx',
                                                 'destination',
                                                 'destination_idx']).astype(np.float32)
        
        matches = matches.drop_duplicates()

        self.matches = matches

    def add_coordinates_to_matches(self):
        """
        Add source and destination x/y columns to the matches dataframe. This
        will add to the overall memory needed to store matches, but makes
        access to x,y easier as a join on the keypoints is not requires.
        """
        skps = self.get_keypoints(self.source, index=self.matches.source_idx)
        dkps = self.get_keypoints(self.destination, index=self.matches.destination_idx)
        self.matches['source_x'] = skps.x.values
        self.matches['source_y'] = skps.y.values
        self.matches['destination_x'] = dkps.x.values
        self.matches['destination_y'] = dkps.y.values

    def project_matches(self, semimajor, semiminor, on='source', srid=None):
        """
        Project matches.
        """
        try:
            coords = self.matches[['{}_y'.format(on),'{}_x'.format(on)]].values
        except:
            self.add_coordinates_to_matches()
            coords = self.matches[['{}_y'.format(on),'{}_x'.format(on)]].values

        node = getattr(self, on)
        camera = getattr(node, 'camera')
        if camera is None:
            warnings.warn('Unable to project matches without a sensor model.')
            return
        
        matches = self.matches
        
        gnd = np.empty((len(coords), 3))
        # Project the points to the surface and reproject into latlon space
        for i in range(gnd.shape[0]):
            gnd[i] = camera.imageToGround(coords[i][0], coords[i][1], 0)
        lon, lat, alt = reproject(gnd.T, semimajor, semiminor,
                                    'geocent', 'latlon')
        if srid:
            geoms = []
            for coord in zip(lon, lat, alt):
                geoms.append('SRID={};POINTZ({} {} {})'.format(srid, coord[0],
                                                                     coord[1],
                                                                     coord[2]))
            matches['geom'] = geoms
        
        matches['lat'] = lat
        matches['lon'] = lon
        self.matches = matches

    def decompose(self):
        """
        Apply coupled decomposition to the images and
@@ -153,6 +267,36 @@ class Edge(dict, MutableMapping):
        pass
        #return.masks[maskname] = od.distance_ratio(matches, **kwargs)

    @utils.methodispatch
    def get_keypoints(self, node, index=None, homogeneous=False, overlap=False):
        if not hasattr(index, '__iter__') and index is not None:
            raise TypeError
        keypts = node.get_keypoint_coordinates(index=index, homogeneous=homogeneous)
        # If the index is passed, the results are returned sorted. The index is not
        # necessarily sorted, so 'unsort' so that the return order matches the passed
        # order
        if index is not None:
            keypts = keypts.reindex(index)
        # If we only want keypoints in the overlap
        if overlap:
            if self.source == node:
                mbr = self['source_mbr']
            else:
                mbr = self['destin_mbr']
            # Can't use overlap if we haven't computed MBRs
            if mbr is None:
                return keypts
            return keypts.query('x >= {} and x <= {} and y >= {} and y <= {}'.format(*mbr))
        return keypts

    @get_keypoints.register(str)
    def _(self, node, index=None, homogeneous=False, overlap=False):
        if not hasattr(index, '__iter__') and index is not None:
            raise TypeError
        node = node.lower()
        node = getattr(self, node)
        return self.get_keypoints(node, index=index, homogeneous=homogeneous, overlap=overlap)
   
    def compute_fundamental_matrix(self, clean_keys=[], maskname='fundamental', **kwargs):
        """
        Estimate the fundamental matrix (F) using the correspondences tagged to this
@@ -173,52 +317,20 @@ class Edge(dict, MutableMapping):
        autocnet.transformation.transformations.FundamentalMatrix

        """
        matches, mask = self.clean(clean_keys)

        # TODO: Homogeneous is horribly inefficient here, use Numpy array notation
        s_keypoints = self.get_keypoints('source', index=matches['source_idx'])
        d_keypoints = self.get_keypoints('destination', index=matches['destination_idx'])

        _, mask = self.clean(clean_keys)
        s_keypoints, d_keypoints = self.get_match_coordinates(clean_keys=clean_keys)
        self.fundamental_matrix, fmask = fm.compute_fundamental_matrix(s_keypoints, d_keypoints, **kwargs)
        
        # Replace the index with the matches index.
        s_keypoints.index = matches.index
        d_keypoints.index = matches.index
        print(fmask)

        self['fundamental_matrix'], fmask = fm.compute_fundamental_matrix(s_keypoints, d_keypoints, **kwargs)

        if isinstance(self['fundamental_matrix'], np.ndarray):
        if isinstance(self.fundamental_matrix, np.ndarray):
            # Convert the truncated RANSAC mask back into a full length mask
            mask[mask] = fmask

            # Set the initial state of the fundamental mask in the masks
            self.masks[maskname] = mask

    @utils.methodispatch
    def get_keypoints(self, node, index=None, homogeneous=False, overlap=False):
        if not hasattr(index, '__iter__') and index is not None:
            raise TypeError
        keypts = node.get_keypoint_coordinates(index=index, homogeneous=homogeneous)
        # If we only want keypoints in the overlap
        if overlap:
            if self.source == node:
                mbr = self['source_mbr']
            else:
                mbr = self['destin_mbr']
            # Can't use overlap if we haven't computed MBRs
            if mbr is None:
                return keypts
            return keypts.query('x >= {} and x <= {} and y >= {} and y <= {}'.format(*mbr))
        return keypts

    @get_keypoints.register(str)
    def _(self, node, index=None, homogeneous=False, overlap=False):
        if not hasattr(index, '__iter__') and index is not None:
            raise TypeError
        node = node.lower()
        node = getattr(self, node)
        return self.get_keypoints(node, index=index, homogeneous=homogeneous, overlap=overlap)

    def compute_fundamental_error(self, clean_keys=[]):
    def compute_fundamental_error(self, method='equality', clean_keys=[]):
        """
        Given a fundamental matrix, compute the reprojective error between
        a two sets of keypoints.
@@ -234,17 +346,17 @@ class Edge(dict, MutableMapping):
        error : pd.Series
                of reprojective error indexed to the matches data frame
        """
        if self['fundamental_matrix'] is None:
        if self.fundamental_matrix is None:
            warnings.warn('No fundamental matrix has been compute for this edge.')
        matches, masks = self.clean(clean_keys)

        source_kps = self.source.get_keypoint_coordinates(index=matches['source_idx'])
        destination_kps = self.destination.get_keypoint_coordinates(index=matches['destination_idx'])

        error = fm.compute_fundamental_error(self['fundamental_matrix'], source_kps, destination_kps)
        matches, mask = self.clean(clean_keys)
        s_keypoints, d_keypoints = self.get_match_coordinates(clean_keys=clean_keys)
        if method == 'equality':
            error = fm.compute_fundamental_error(self.fundamental_matrix, s_keypoints, d_keypoints)
        elif method == 'projection':
            error = fm.compute_reprojection_error(self.fundamental_matrix, s_keypoints, d_keypoints)

        error = pd.Series(error, index=matches.index)
        return error
        self.costs.loc[mask, 'fundamental_{}'.format(method)] = error

    def compute_homography(self, method='ransac', clean_keys=[], pid=None, maskname='homography', **kwargs):
        """
@@ -276,9 +388,8 @@ class Edge(dict, MutableMapping):
        mask[mask] = hmask
        self.masks['homography'] = mask

    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):
    def subpixel_register(self, method='phase', clean_keys=[],
                          template_size=251, search_size=251, **kwargs):
        """
        For the entire graph, compute the subpixel offsets using pattern-matching and add the result
        as an attribute to each edge of the graph.
@@ -312,59 +423,69 @@ class Edge(dict, MutableMapping):
                      The maximum (positive) value that a pixel can shift in the y direction
                      without being considered an outlier
        """
        for column, default in {'x_offset': 0, 'y_offset': 0, 'correlation': 0, 'reference': -1}.items():
            if column not in self.subpixel_matches.columns:
                self.subpixel_matches[column] = default

        # Build up a composite mask from all of the user specified masks
        matches, mask = self.clean(clean_keys)

        # Grab the full images, or handles
        if tiled is True:
        # Get the img handles
        s_img = self.source.geodata
        d_img = self.destination.geodata
        else:
            s_img = self.source.geodata.read_array()
            d_img = self.destination.geodata.read_array()

        source_image = (matches.iloc[0]['source_image'])
        # Determine which algorithm is going ot be used.
        if method == 'phase':
            func = sp.subpixel_phase
            shifts_x, shifts_y, strengths, new_x, new_y = sp._prep_subpixel(len(matches), 2)
        elif method == 'template':
            func = sp.subpixel_template
            shifts_x, shifts_y, strengths, new_x, new_y = sp._prep_subpixel(len(matches), 1)

        pts = []
        # for each edge, calculate this for each keypoint pair
        for i, (idx, row) in enumerate(matches.iterrows()):
            s_idx = int(row['source_idx'])
            d_idx = int(row['destination_idx'])

            s_keypoint = self.source.get_keypoint_coordinates(s_idx)
            d_keypoint = self.destination.get_keypoint_coordinates(d_idx)

            # Get the template and search window
            s_template = sp.clip_roi(s_img, s_keypoint, template_size)
            d_search = sp.clip_roi(d_img, d_keypoint, search_size)
            if 0 in s_template.shape or 0 in d_search.shape:
                continue
            try:
                (x_offset, y_offset, strength),ref = sp.subpixel_offset(s_template, d_search, **kwargs)
                self.subpixel_matches.loc[idx, ('x_offset', 'y_offset', 'correlation', 'reference')]= [x_offset, y_offset, strength, source_image]
                pts.append([s_template, d_search, ref, x_offset, y_offset])
            except:
                warnings.warn('Template-Search size mismatch, failing for this correspondence point.')
            s_keypoint = self.source.get_keypoint_coordinates([s_idx])
            d_keypoint = self.destination.get_keypoint_coordinates([d_idx])

            s_template, sx, sy = sp.clip_roi(s_img, s_keypoint.x, s_keypoint.y,
                                     size_x=template_size, size_y=template_size)
            d_search, dx, dy = sp.clip_roi(d_img, d_keypoint.x, d_keypoint.y,
                                   size_x=search_size, size_y=search_size)
            
            # Now check to see if these are the same size.
            if method == 'phase' and (s_template.shape != d_search.shape):
                s_size = s_template.shape
                d_size = d_search.shape
                updated_size = int(min(s_size + d_size) / 2)
                s_template, sx, sy = sp.clip_roi(s_img, s_keypoint.x, s_keypoint.y,
                                     size_x=updated_size, size_y=updated_size)
                d_search, dx, dy = sp.clip_roi(d_img, d_keypoint.x, d_keypoint.y,
                                    size_x=updated_size, size_y=updated_size)         
            
            shift_x, shift_y, metrics = func(s_template, d_search, **kwargs)

            # ROIs and clipping all work using whole pixels. The clip_roi func returns
            # the subpixel components that are lost when converting to whole pixels
            # reapply those here.
            shift_x += dx
            shift_y += dy

            shifts_x[i] = shift_x
            shifts_y[i] = shift_y
            new_x[i] = d_keypoint.x - shift_x
            new_y[i] = d_keypoint.y - shift_y
            strengths[i] = metrics
        
        self.matches.loc[mask, 'shift_x'] = shifts_x
        self.matches.loc[mask, 'shift_y'] = shifts_y
        self.matches.loc[mask, 'destination_x'] = new_x
        self.matches.loc[mask, 'destination_y'] = new_y

        if method == 'phase':
            self.costs.loc[mask, 'phase'] = [i[0] for i in strengths]
            self.costs.loc[mask, 'rmse'] = [i[1] for i in strengths]
        elif method == 'template':
            self.costs.loc[mask, 'correlation'] = strengths
 
        # Compute the mask for correlations less than the threshold
        threshold_mask = self.subpixel_matches['correlation'] >= threshold

        # Compute the mask for the point shifts that are too large
        query_string = 'x_offset <= -{0} or x_offset >= {0} or y_offset <= -{1} or y_offset >= {1}'.format(max_x_shift,max_y_shift)
        sp_shift_outliers = self.subpixel_matches.query(query_string)
        shift_mask = pd.Series(True, index=self.subpixel_matches.index)
        shift_mask.loc[sp_shift_outliers.index] = False

        # Generate the composite mask and write the masks to the mask data structure
        mask = threshold_mask & shift_mask
        self.masks['shift'] = shift_mask
        self.masks['threshold'] = threshold_mask
        self.masks['subpixel'] = mask
        return pts

    def suppress(self, suppression_func=spf.correlation, clean_keys=[], maskname='suppression', **kwargs):
        """
@@ -553,17 +674,20 @@ class Edge(dict, MutableMapping):
        self['source_mbr'] = smbr
        self['destin_mbr'] = dmbr

    def get_match_coordinates(self, clean_keys=[]):
        matches = self.get_matches(clean_keys=clean_keys)
        skps = matches[['source_x', 'source_y']]
        dkps = matches[['destination_x', 'destination_y']]

        return skps, dkps

    def get_matches(self, clean_keys=[]): # pragma: no cover
        if self.matches.empty:
            return pd.DataFrame()

        match, _ = self.clean(clean_keys=clean_keys)
        match = match[['source_image', 'source_idx',
                       'destination_image', 'destination_idx']]
        skps = self.get_keypoints('source', index=match.source_idx)
        skps.columns = ['source_x', 'source_y']
        dkps = self.get_keypoints('destination', index=match.destination_idx)
        dkps.columns = ['destination_x', 'destination_y']
        match = match.join(skps, on='source_idx')
        match = match.join(dkps, on='destination_idx')
        return match
        self.add_coordinates_to_matches()
        matches, _ = self.clean(clean_keys=clean_keys)
        skps = matches[['source_x', 'source_y']]
        dkps = matches[['destination_x', 'destination_y']]
        
        return matches
 No newline at end of file
+26 −8
Original line number Diff line number Diff line
@@ -94,6 +94,13 @@ class CandidateGraph(nx.Graph):
        if overlaps:
            self.compute_overlaps()

    def __key(self):
        # TODO: This needs to be a real self identifying key
        return 'abcde'

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

    def __eq__(self, other):
        # Check the nodes
        if sorted(self.nodes()) != sorted(other.nodes()):
@@ -124,6 +131,19 @@ class CandidateGraph(nx.Graph):
        else:
            self._maxsize = MAXSIZE[value]

    @property
    def unmatched_edges(self):
        """
        Returns a list of edges (source, destination) that do not have
        entries in the matches dataframe.
        """
        unmatched = []
        for s, d, e in self.edges(data='data'):
            if len(e.matches) == 0:
                unmatched.append((s,d))

        return unmatched

    @classmethod
    def from_filelist(cls, filelist, basepath=None):
        """
@@ -352,14 +372,14 @@ class CandidateGraph(nx.Graph):
                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): #pragma: no cover
        for i, node in self.nodes(data='data'):
            print('Processing {}'.format(node['image_name']))
            node.extract_features_with_tiling(tilesize=tilesize, overlap=overlap, *args, **kwargs)
    def extract_features_with_tiling(self, *args, **kwargs): #pragma: no cover
        """

    def save_features(self, out_path):
        """
        self.apply(Node.extract_features_with_tiling, args=args, **kwargs)

    def save_features(self, out_path):
        """
        Save the features (keypoints and descriptors) for the
        specified nodes.

@@ -369,9 +389,6 @@ class CandidateGraph(nx.Graph):
                   Location of the output file.  If the file exists,
                   features are appended.  Otherwise, the file is created.
        """



        self.apply(Node.save_features, args=(out_path,), on='node')

    def load_features(self, in_path, nodes=[], nfeatures=None, **kwargs):
@@ -388,6 +405,7 @@ class CandidateGraph(nx.Graph):
                of nodes to load features for.  If empty, load features
                for all nodes
        """
        self.apply(Nodes.load_features, args=(in_path, nfeatures), on='node', **kwargs)
        for n in self.nodes:
            if node['node_id'] not in nodes:
                continue
+81 −17

File changed.

Preview size limit exceeded, changes collapsed.

+17 −3
Original line number Diff line number Diff line
@@ -47,7 +47,6 @@ def from_hdf(in_path, index=None, keypoints=True, descriptors=True):
    outd = '/descriptors'
    outk = '/keypoints'


    if index is not None:
        index=np.asarray(index)

@@ -69,6 +68,8 @@ def from_hdf(in_path, index=None, keypoints=True, descriptors=True):
            desc = hdf[outd][:]
        if keypoints:
            raw_kps = hdf[outk][:]
    
    if keypoints:
        index = raw_kps['index']
        clean_kps = utils.remove_field_name(raw_kps, 'index')
        columns = clean_kps.dtype.names
@@ -86,7 +87,7 @@ def from_hdf(in_path, index=None, keypoints=True, descriptors=True):
        return desc


def to_hdf(keypoints, descriptors, out_path, key=None):
def to_hdf(out_path, keypoints=None, descriptors=None, key=None):
    """
    Save keypoints and descriptors to HDF at a given out_path at either
    the root or at some arbitrary path given by a key.
@@ -108,20 +109,33 @@ def to_hdf(keypoints, descriptors, out_path, key=None):
    """
    # If the out_path is a string, access the HDF5 file
    if isinstance(out_path, str):
        hdf = io_hdf.HDFDataset(out_path, mode='w')
        hdf = io_hdf.HDFDataset(out_path, mode='a')
    else:
        hdf = out_path

    grps = list(hdf.keys())

    outd = '/descriptors'
    outk = '/keypoints'
    if descriptors is not None:
        # Strip the leading slash
        if outd[1:] in grps:
            del hdf[outd] # Prep to replace

        hdf.create_dataset(outd,
                        data=descriptors,
                        compression=io_hdf.DEFAULT_COMPRESSION,
                        compression_opts=io_hdf.DEFAULT_COMPRESSION_VALUE)

    if keypoints is not None:
        if outk[1:] in grps:
            del hdf[outk]

        hdf.create_dataset(outk,
                        data=hdf.df_to_sarray(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']))

+2 −2
Original line number Diff line number Diff line
@@ -25,7 +25,7 @@ def test_read_write_npy(tmpdir, kd):
def test_read_write_hdf(tmpdir, kd):
    kps, desc = kd
    path = tmpdir.join('out.h5')
    keypoints.to_hdf(kps, desc, path.strpath)
    keypoints.to_hdf(path.strpath, keypoints=kps, descriptors=desc)
    reloaded_kps, reloaded_desc = keypoints.from_hdf(path.strpath)

    assert reloaded_kps.equals(kps)
@@ -35,7 +35,7 @@ def test_read_write_hdf_with_live_file(tmpdir, kd):
    kps, desc = kd
    path = tmpdir.join('live.h5')
    hf = io_hdf.HDFDataset(path.strpath, mode='w')
    keypoints.to_hdf(kps, desc, hf)
    keypoints.to_hdf(hf, keypoints=kps, descriptors=desc)
    reloaded_kps, reloaded_desc = keypoints.from_hdf(hf)

    assert reloaded_kps.equals(kps)
Loading