Robust matching using RANSACΒΆ

In this simplified example we first generate two synthetic images as if they were taken from different view points.

In the next step we find interest points in both images and find correspondences based on a weighted sum of squared differences of a small neighborhood around them. Note, that this measure is only robust towards linear radiometric and not geometric distortions and is thus only usable with slight view point changes.

After finding the correspondences we end up having a set of source and destination coordinates which can be used to estimate the geometric transformation between both images. However, many of the correspondences are faulty and simply estimating the parameter set with all coordinates is not sufficient. Therefore, the RANSAC algorithm is used on top of the normal model to robustly estimate the parameter set by detecting outliers.

../_images/plot_matching_1.png

from __future__ import print_function

import numpy as np
from matplotlib import pyplot as plt

from skimage import data
from skimage.util import img_as_float
from skimage.feature import (corner_harris, corner_subpix, corner_peaks,
                             plot_matches)
from skimage.transform import warp, AffineTransform
from skimage.exposure import rescale_intensity
from skimage.color import rgb2gray
from skimage.measure import ransac


# generate synthetic checkerboard image and add gradient for the later matching
checkerboard = img_as_float(data.checkerboard())
img_orig = np.zeros(list(checkerboard.shape) + [3])
img_orig[..., 0] = checkerboard
gradient_r, gradient_c = np.mgrid[0:img_orig.shape[0],
                                  0:img_orig.shape[1]] / float(img_orig.shape[0])
img_orig[..., 1] = gradient_r
img_orig[..., 2] = gradient_c
img_orig = rescale_intensity(img_orig)
img_orig_gray = rgb2gray(img_orig)

# warp synthetic image
tform = AffineTransform(scale=(0.9, 0.9), rotation=0.2, translation=(20, -10))
img_warped = warp(img_orig, tform.inverse, output_shape=(200, 200))
img_warped_gray = rgb2gray(img_warped)

# extract corners using Harris' corner measure
coords_orig = corner_peaks(corner_harris(img_orig_gray), threshold_rel=0.001,
                           min_distance=5)
coords_warped = corner_peaks(corner_harris(img_warped_gray),
                             threshold_rel=0.001, min_distance=5)

# determine sub-pixel corner position
coords_orig_subpix = corner_subpix(img_orig_gray, coords_orig, window_size=9)
coords_warped_subpix = corner_subpix(img_warped_gray, coords_warped,
                                     window_size=9)


def gaussian_weights(window_ext, sigma=1):
    y, x = np.mgrid[-window_ext:window_ext+1, -window_ext:window_ext+1]
    g = np.zeros(y.shape, dtype=np.double)
    g[:] = np.exp(-0.5 * (x**2 / sigma**2 + y**2 / sigma**2))
    g /= 2 * np.pi * sigma * sigma
    return g


def match_corner(coord, window_ext=5):
    r, c =  np.round(coord).astype(np.intp)
    window_orig = img_orig[r-window_ext:r+window_ext+1,
                           c-window_ext:c+window_ext+1, :]

    # weight pixels depending on distance to center pixel
    weights = gaussian_weights(window_ext, 3)
    weights = np.dstack((weights, weights, weights))

    # compute sum of squared differences to all corners in warped image
    SSDs = []
    for cr, cc in coords_warped:
        window_warped = img_warped[cr-window_ext:cr+window_ext+1,
                                   cc-window_ext:cc+window_ext+1, :]
        SSD = np.sum(weights * (window_orig - window_warped)**2)
        SSDs.append(SSD)

    # use corner with minimum SSD as correspondence
    min_idx = np.argmin(SSDs)
    return coords_warped_subpix[min_idx]


# find correspondences using simple weighted sum of squared differences
src = []
dst = []
for coord in coords_orig_subpix:
    src.append(coord)
    dst.append(match_corner(coord))
src = np.array(src)
dst = np.array(dst)


# estimate affine transform model using all coordinates
model = AffineTransform()
model.estimate(src, dst)

# robustly estimate affine transform model with RANSAC
model_robust, inliers = ransac((src, dst), AffineTransform, min_samples=3,
                               residual_threshold=2, max_trials=100)
outliers = inliers == False


# compare "true" and estimated transform parameters
print(tform.scale, tform.translation, tform.rotation)
print(model.scale, model.translation, model.rotation)
print(model_robust.scale, model_robust.translation, model_robust.rotation)

# visualize correspondence
fig, ax = plt.subplots(nrows=2, ncols=1)

plt.gray()

inlier_idxs = np.nonzero(inliers)[0]
plot_matches(ax[0], img_orig_gray, img_warped_gray, src, dst,
             np.column_stack((inlier_idxs, inlier_idxs)), matches_color='b')
ax[0].axis('off')
ax[0].set_title('Correct correspondences')

outlier_idxs = np.nonzero(outliers)[0]
plot_matches(ax[1], img_orig_gray, img_warped_gray, src, dst,
             np.column_stack((outlier_idxs, outlier_idxs)), matches_color='r')
ax[1].axis('off')
ax[1].set_title('Faulty correspondences')

plt.show()

STDOUT


        

STDERR


        

Python source code: download (generated using skimage 0.11dev)

IPython Notebook: download (generated using skimage 0.11dev)

ZnJvbSBfX2Z1dHVyZV9fIGltcG9ydCBwcmludF9mdW5jdGlvbgoKaW1wb3J0IG51bXB5IGFzIG5wCmZyb20gbWF0cGxvdGxpYiBpbXBvcnQgcHlwbG90IGFzIHBsdAoKZnJvbSBza2ltYWdlIGltcG9ydCBkYXRhCmZyb20gc2tpbWFnZS51dGlsIGltcG9ydCBpbWdfYXNfZmxvYXQKZnJvbSBza2ltYWdlLmZlYXR1cmUgaW1wb3J0IChjb3JuZXJfaGFycmlzLCBjb3JuZXJfc3VicGl4LCBjb3JuZXJfcGVha3MsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcGxvdF9tYXRjaGVzKQpmcm9tIHNraW1hZ2UudHJhbnNmb3JtIGltcG9ydCB3YXJwLCBBZmZpbmVUcmFuc2Zvcm0KZnJvbSBza2ltYWdlLmV4cG9zdXJlIGltcG9ydCByZXNjYWxlX2ludGVuc2l0eQpmcm9tIHNraW1hZ2UuY29sb3IgaW1wb3J0IHJnYjJncmF5CmZyb20gc2tpbWFnZS5tZWFzdXJlIGltcG9ydCByYW5zYWMKCgojIGdlbmVyYXRlIHN5bnRoZXRpYyBjaGVja2VyYm9hcmQgaW1hZ2UgYW5kIGFkZCBncmFkaWVudCBmb3IgdGhlIGxhdGVyIG1hdGNoaW5nCmNoZWNrZXJib2FyZCA9IGltZ19hc19mbG9hdChkYXRhLmNoZWNrZXJib2FyZCgpKQppbWdfb3JpZyA9IG5wLnplcm9zKGxpc3QoY2hlY2tlcmJvYXJkLnNoYXBlKSArIFszXSkKaW1nX29yaWdbLi4uLCAwXSA9IGNoZWNrZXJib2FyZApncmFkaWVudF9yLCBncmFkaWVudF9jID0gbnAubWdyaWRbMDppbWdfb3JpZy5zaGFwZVswXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDA6aW1nX29yaWcuc2hhcGVbMV1dIC8gZmxvYXQoaW1nX29yaWcuc2hhcGVbMF0pCmltZ19vcmlnWy4uLiwgMV0gPSBncmFkaWVudF9yCmltZ19vcmlnWy4uLiwgMl0gPSBncmFkaWVudF9jCmltZ19vcmlnID0gcmVzY2FsZV9pbnRlbnNpdHkoaW1nX29yaWcpCmltZ19vcmlnX2dyYXkgPSByZ2IyZ3JheShpbWdfb3JpZykKCiMgd2FycCBzeW50aGV0aWMgaW1hZ2UKdGZvcm0gPSBBZmZpbmVUcmFuc2Zvcm0oc2NhbGU9KDAuOSwgMC45KSwgcm90YXRpb249MC4yLCB0cmFuc2xhdGlvbj0oMjAsIC0xMCkpCmltZ193YXJwZWQgPSB3YXJwKGltZ19vcmlnLCB0Zm9ybS5pbnZlcnNlLCBvdXRwdXRfc2hhcGU9KDIwMCwgMjAwKSkKaW1nX3dhcnBlZF9ncmF5ID0gcmdiMmdyYXkoaW1nX3dhcnBlZCkKCiMgZXh0cmFjdCBjb3JuZXJzIHVzaW5nIEhhcnJpcycgY29ybmVyIG1lYXN1cmUKY29vcmRzX29yaWcgPSBjb3JuZXJfcGVha3MoY29ybmVyX2hhcnJpcyhpbWdfb3JpZ19ncmF5KSwgdGhyZXNob2xkX3JlbD0wLjAwMSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgbWluX2Rpc3RhbmNlPTUpCmNvb3Jkc193YXJwZWQgPSBjb3JuZXJfcGVha3MoY29ybmVyX2hhcnJpcyhpbWdfd2FycGVkX2dyYXkpLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRocmVzaG9sZF9yZWw9MC4wMDEsIG1pbl9kaXN0YW5jZT01KQoKIyBkZXRlcm1pbmUgc3ViLXBpeGVsIGNvcm5lciBwb3NpdGlvbgpjb29yZHNfb3JpZ19zdWJwaXggPSBjb3JuZXJfc3VicGl4KGltZ19vcmlnX2dyYXksIGNvb3Jkc19vcmlnLCB3aW5kb3dfc2l6ZT05KQpjb29yZHNfd2FycGVkX3N1YnBpeCA9IGNvcm5lcl9zdWJwaXgoaW1nX3dhcnBlZF9ncmF5LCBjb29yZHNfd2FycGVkLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgd2luZG93X3NpemU9OSkKCgpkZWYgZ2F1c3NpYW5fd2VpZ2h0cyh3aW5kb3dfZXh0LCBzaWdtYT0xKToKICAgIHksIHggPSBucC5tZ3JpZFstd2luZG93X2V4dDp3aW5kb3dfZXh0KzEsIC13aW5kb3dfZXh0OndpbmRvd19leHQrMV0KICAgIGcgPSBucC56ZXJvcyh5LnNoYXBlLCBkdHlwZT1ucC5kb3VibGUpCiAgICBnWzpdID0gbnAuZXhwKC0wLjUgKiAoeCoqMiAvIHNpZ21hKioyICsgeSoqMiAvIHNpZ21hKioyKSkKICAgIGcgLz0gMiAqIG5wLnBpICogc2lnbWEgKiBzaWdtYQogICAgcmV0dXJuIGcKCgpkZWYgbWF0Y2hfY29ybmVyKGNvb3JkLCB3aW5kb3dfZXh0PTUpOgogICAgciwgYyA9ICBucC5yb3VuZChjb29yZCkuYXN0eXBlKG5wLmludHApCiAgICB3aW5kb3dfb3JpZyA9IGltZ19vcmlnW3Itd2luZG93X2V4dDpyK3dpbmRvd19leHQrMSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgYy13aW5kb3dfZXh0OmMrd2luZG93X2V4dCsxLCA6XQoKICAgICMgd2VpZ2h0IHBpeGVscyBkZXBlbmRpbmcgb24gZGlzdGFuY2UgdG8gY2VudGVyIHBpeGVsCiAgICB3ZWlnaHRzID0gZ2F1c3NpYW5fd2VpZ2h0cyh3aW5kb3dfZXh0LCAzKQogICAgd2VpZ2h0cyA9IG5wLmRzdGFjaygod2VpZ2h0cywgd2VpZ2h0cywgd2VpZ2h0cykpCgogICAgIyBjb21wdXRlIHN1bSBvZiBzcXVhcmVkIGRpZmZlcmVuY2VzIHRvIGFsbCBjb3JuZXJzIGluIHdhcnBlZCBpbWFnZQogICAgU1NEcyA9IFtdCiAgICBmb3IgY3IsIGNjIGluIGNvb3Jkc193YXJwZWQ6CiAgICAgICAgd2luZG93X3dhcnBlZCA9IGltZ193YXJwZWRbY3Itd2luZG93X2V4dDpjcit3aW5kb3dfZXh0KzEsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY2Mtd2luZG93X2V4dDpjYyt3aW5kb3dfZXh0KzEsIDpdCiAgICAgICAgU1NEID0gbnAuc3VtKHdlaWdodHMgKiAod2luZG93X29yaWcgLSB3aW5kb3dfd2FycGVkKSoqMikKICAgICAgICBTU0RzLmFwcGVuZChTU0QpCgogICAgIyB1c2UgY29ybmVyIHdpdGggbWluaW11bSBTU0QgYXMgY29ycmVzcG9uZGVuY2UKICAgIG1pbl9pZHggPSBucC5hcmdtaW4oU1NEcykKICAgIHJldHVybiBjb29yZHNfd2FycGVkX3N1YnBpeFttaW5faWR4XQoKCiMgZmluZCBjb3JyZXNwb25kZW5jZXMgdXNpbmcgc2ltcGxlIHdlaWdodGVkIHN1bSBvZiBzcXVhcmVkIGRpZmZlcmVuY2VzCnNyYyA9IFtdCmRzdCA9IFtdCmZvciBjb29yZCBpbiBjb29yZHNfb3JpZ19zdWJwaXg6CiAgICBzcmMuYXBwZW5kKGNvb3JkKQogICAgZHN0LmFwcGVuZChtYXRjaF9jb3JuZXIoY29vcmQpKQpzcmMgPSBucC5hcnJheShzcmMpCmRzdCA9IG5wLmFycmF5KGRzdCkKCgojIGVzdGltYXRlIGFmZmluZSB0cmFuc2Zvcm0gbW9kZWwgdXNpbmcgYWxsIGNvb3JkaW5hdGVzCm1vZGVsID0gQWZmaW5lVHJhbnNmb3JtKCkKbW9kZWwuZXN0aW1hdGUoc3JjLCBkc3QpCgojIHJvYnVzdGx5IGVzdGltYXRlIGFmZmluZSB0cmFuc2Zvcm0gbW9kZWwgd2l0aCBSQU5TQUMKbW9kZWxfcm9idXN0LCBpbmxpZXJzID0gcmFuc2FjKChzcmMsIGRzdCksIEFmZmluZVRyYW5zZm9ybSwgbWluX3NhbXBsZXM9MywKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJlc2lkdWFsX3RocmVzaG9sZD0yLCBtYXhfdHJpYWxzPTEwMCkKb3V0bGllcnMgPSBpbmxpZXJzID09IEZhbHNlCgoKIyBjb21wYXJlICJ0cnVlIiBhbmQgZXN0aW1hdGVkIHRyYW5zZm9ybSBwYXJhbWV0ZXJzCnByaW50KHRmb3JtLnNjYWxlLCB0Zm9ybS50cmFuc2xhdGlvbiwgdGZvcm0ucm90YXRpb24pCnByaW50KG1vZGVsLnNjYWxlLCBtb2RlbC50cmFuc2xhdGlvbiwgbW9kZWwucm90YXRpb24pCnByaW50KG1vZGVsX3JvYnVzdC5zY2FsZSwgbW9kZWxfcm9idXN0LnRyYW5zbGF0aW9uLCBtb2RlbF9yb2J1c3Qucm90YXRpb24pCgojIHZpc3VhbGl6ZSBjb3JyZXNwb25kZW5jZQpmaWcsIGF4ID0gcGx0LnN1YnBsb3RzKG5yb3dzPTIsIG5jb2xzPTEpCgpwbHQuZ3JheSgpCgppbmxpZXJfaWR4cyA9IG5wLm5vbnplcm8oaW5saWVycylbMF0KcGxvdF9tYXRjaGVzKGF4WzBdLCBpbWdfb3JpZ19ncmF5LCBpbWdfd2FycGVkX2dyYXksIHNyYywgZHN0LAogICAgICAgICAgICAgbnAuY29sdW1uX3N0YWNrKChpbmxpZXJfaWR4cywgaW5saWVyX2lkeHMpKSwgbWF0Y2hlc19jb2xvcj0nYicpCmF4WzBdLmF4aXMoJ29mZicpCmF4WzBdLnNldF90aXRsZSgnQ29ycmVjdCBjb3JyZXNwb25kZW5jZXMnKQoKb3V0bGllcl9pZHhzID0gbnAubm9uemVybyhvdXRsaWVycylbMF0KcGxvdF9tYXRjaGVzKGF4WzFdLCBpbWdfb3JpZ19ncmF5LCBpbWdfd2FycGVkX2dyYXksIHNyYywgZHN0LAogICAgICAgICAgICAgbnAuY29sdW1uX3N0YWNrKChvdXRsaWVyX2lkeHMsIG91dGxpZXJfaWR4cykpLCBtYXRjaGVzX2NvbG9yPSdyJykKYXhbMV0uYXhpcygnb2ZmJykKYXhbMV0uc2V0X3RpdGxlKCdGYXVsdHkgY29ycmVzcG9uZGVuY2VzJykKCnBsdC5zaG93KCk=