Matching

[1]:
import math
import os
import sys

sys.path.insert(0, os.path.abspath('/data/autocnet'))

import autocnet
from autocnet import CandidateGraph

# The GPU based extraction library that contains SIFT extraction and matching
import cudasift as cs

# A method to resize the images on the fly.
from scipy.misc import imresize

%pylab inline
figsize(16,4)
Populating the interactive namespace from numpy and matplotlib

CandidateGraph -> Custom Extraction Func -> Matching

[8]:
a = 'AS15-P-0111_CENTER_LRG_CROPPED.png'
b = 'AS15-P-0112_CENTER_LRG_CROPPED.png'

adj = {a:[b],
       b:[a]}

cg = CandidateGraph.from_adjacency(adj)

# Enable the GPU
autocnet.cuda(enable=True, gpu=0)

# Write a custom keypoint extraction function - this could get monkey patched onto the graph object...
def extract(arr, downsample_amount=None, **kwargs):
    total_size = arr.shape[0] * arr.shape[1]
    if not downsample_amount:
        downsample_amount = math.ceil(total_size / 12500**2)
    shape = (int(arr.shape[0] / downsample_amount), int(arr.shape[1] / downsample_amount))
    # Downsample
    arr = imresize(arr, shape, interp='lanczos')

    npts = max(arr0.shape) / 3.5
    sd = cs.PySiftData(npts)
    cs.ExtractKeypoints(arr, sd, **kwargs)
    kp, des = sd.to_data_frame()
    kp = kp[['x', 'y', 'scale', 'sharpness', 'edgeness', 'orientation', 'score', 'ambiguity']]
    kp['score'] = 0.0
    kp['ambiguity'] = 0.0

    return kp, des, sd, downsample_amount, arr

arr0 = cg.node[0].geodata.read_array()
kp0, des0, sd0, downsample_amount0, arr0 = extract(arr0, thresh=1)

arr1 = cg.node[1].geodata.read_array()
kp1, des1, sd1, downsample_amount1, arr1 = extract(arr1, thresh=1)

[9]:
# How much downsampling?
downsample_amount0, downsample_amount1
[9]:
(5, 5)

Now that the keypoints have been extracted it is time to apply the GPU based matcher. Since this is working with the lower level API, the correspondences are read from the GPU and then piped back. This is technical inefficient, but fast enough that saving a few lines of code managing GPU memory is worth it.

[10]:
sd0 = cs.PySiftData.from_data_frame(kp0, des0)
sd1 = cs.PySiftData.from_data_frame(kp1, des1)
# Apply the matcher
cs.PyMatchSiftData(sd0, sd1)

The matches DataFrame

Matches are stored as Pandas DataFrames meaning that all functionality available to a Pandas DataFrame is available to the matches. This includes:

  • Visualization

  • Sorting

  • SQL style queries

  • Split-Apply-Combine Operations

  • Statistical Analysis

[11]:
matches, _ = sd0.to_data_frame()
matches.head(3)
[11]:
x y scale sharpness edgeness orientation score ambiguity match match_xpos match_ypos match_error subsampling
0 9528.347656 195.259979 20.769804 3.171909 4.874205 191.425842 0.952312 0.998823 1969 7409.627930 464.366821 0.0 0.0
1 4080.437012 66.662041 16.442673 -1.066230 5.038958 272.796051 0.978477 0.999569 351 7551.666992 42.322033 0.0 0.0
2 5775.711914 140.684418 21.859934 2.915606 4.006656 247.030746 0.936110 0.949781 21 3847.362793 140.941910 0.0 0.0

Position, Ambiguity, and Score

Three important pieces of information in the matches dataframe are the x/y position of correspondences, the ambiguity, and the score.

Position: The position of the correspondence in arr0 (assuming matches points to sd0) is the x and y position. The correspondence position in the other image is defined by match_xpos and match_ypos.

Ambiguity: Ambiguity is a measure of Lowe’s ratio test that compares the ‘likeness’ of correspondences. In brief, during matching the top two matches are found. Meaning correspondence 0 in the source image is matched to 2 candidate correspondences in the other image. The quality of these matches is defined as the distance between two 128 element vectors (the descriptors). Ambiguity is a measure of the ratio of the second distance to the first distance. The closer this value is to 1, the more ambiguous the correspondence. The lower the ambiguity the better. Lowe suggests 0.8 as a threshold, but this is too constraining for highly homogeneous planetary data.

Score: Score is a measure of the quality of the match scaled to the range [0,1] from the inital, unknown range of Euclidean distances. The higher the score the better.

[12]:
# Box plot and descriptive stats for the distribution of ambiguities
matches.boxplot('ambiguity')
matches['ambiguity'].describe()
[12]:
count    17062.000000
mean         0.991214
std          0.010747
min          0.789020
25%          0.988632
50%          0.994540
75%          0.997817
max          0.999998
Name: ambiguity, dtype: float64
../../../_images/users_tutorials_apollopan_3._Matching_10_1.png

Clearly, Lowe’s suggested 0.8 ratio is going to remove almost all of the data. The 0.95 threshold looks attractive as it is below the lower quartile break.

[13]:
ksub = matches[matches.ambiguity <= 0.95]
[14]:
# And plot
imshow(arr0)
plot(ksub.x, ksub.y, 'ro')
show()
imshow(arr1)
plot(ksub.match_xpos, ksub.match_ypos, 'bo')
show()
../../../_images/users_tutorials_apollopan_3._Matching_13_0.png
../../../_images/users_tutorials_apollopan_3._Matching_13_1.png

Score

[16]:
ksub.boxplot(column='score')
ksub.score.describe()
[16]:
count    163.000000
mean       0.898343
std        0.048708
min        0.775125
25%        0.866216
50%        0.897366
75%        0.929772
max        0.989991
Name: score, dtype: float64
../../../_images/users_tutorials_apollopan_3._Matching_15_1.png
[17]:
kss = ksub[ksub.score >= 0.925310]
imshow(arr0)
plot(kss.x, kss.y, 'ro')
show()
imshow(arr1)
plot(kss.match_xpos, kss.match_ypos, 'bo')
show()
../../../_images/users_tutorials_apollopan_3._Matching_16_0.png
../../../_images/users_tutorials_apollopan_3._Matching_16_1.png

The combined reduction due to ambiguity and score winnowing has generated what looks like a relatively good starting point for more robust outlier detection methods.