SIFT image alignment tutorial#

SIFT (Scale-Invariant Feature Transform) is an algorithm developped by David Lowe in 1999. It is a worldwide reference for image alignment and object recognition. The robustness of this method enables to detect features at different scales, angles and illumination of a scene.

Silx provides an implementation of SIFT in OpenCL, meaning that it can run on Graphics Processing Units and Central Processing Units as well. Interest points are detected in the image, then data structures called descriptors are built to be characteristic of the scene, so that two different images of the same scene have similar descriptors. They are robust to transformations like translation, rotation, rescaling and illumination change, which make SIFT interesting for image stitching.

In the fist stage, descriptors are computed from the input images. Then, they are compared to determine the geometric transformation to apply in order to align the images. This implementation can run on most graphic cards and CPU, making it usable on many setups. OpenCL processes are handled from Python with PyOpenCL, a module to access OpenCL parallel computation API.

This tutuorial explains the three subsequent steps:

  • keypoint extraction

  • Keypoint matching

  • image alignment

All the tutorial has been made using the Jupyter notebook.

[1]:
import time

start_time = time.time()
%pylab nbagg
Populating the interactive namespace from numpy and matplotlib
[2]:
# display test image
import silx

print("Silx version %s" % silx.version)

from PIL import Image
from silx.test.utils import utilstest

path = utilstest.getfile("lena.png")
image = numpy.asarray(Image.open(path))
fig, ax = subplots()
ax.imshow(image)
Silx version 0.7.0-dev0
[2]:
<matplotlib.image.AxesImage at 0x7fa76a2fa9e8>
[3]:
# Initialization of the sift object is time consuming: it compiles all the code.
import os

# set to 1 to see the compilation going on
os.environ["PYOPENCL_COMPILER_OUTPUT"] = "0"
# switch to "GPU" to "CPU" to enable fail-save version.
devicetype = "GPU"
from silx.image import sift

%time sift_ocl = sift.SiftPlan(template=image, devicetype=devicetype)

print("Device used for calculation: ", sift_ocl.ctx.devices[0].name)
CPU times: user 680 ms, sys: 236 ms, total: 916 ms
Wall time: 919 ms
Device used for calculation:  TITAN V
[4]:
print("Time for calculating the keypoints on one image of size %sx%s" % image.shape[:2])
%time keypoints = sift_ocl(image)
print("Number of keypoints: %s" % len(keypoints))
print("Keypoint content:")
print(keypoints.dtype)
print(
    "x: %.3f \t y: %.3f \t sigma: %.3f \t angle: %.3f"
    % (keypoints[-1].x, keypoints[-1].y, keypoints[-1].scale, keypoints[-1].angle)
)
print("descriptor:")
print(keypoints[-1].desc)
Time for calculating the keypoints on one image of size 512x512
CPU times: user 20 ms, sys: 4 ms, total: 24 ms
Wall time: 23.6 ms
Number of keypoints: 411
Keypoint content:
(numpy.record, [('x', '<f4'), ('y', '<f4'), ('scale', '<f4'), ('angle', '<f4'), ('desc', 'u1', (128,))])
x: 275.483       y: 302.585      sigma: 36.518   angle: -0.194
descriptor:
[ 11   5   0   1   5  20  22   8  88  20   3   0   0   4  40 120  41   9
  13  52  32  36  15  81   1   8  14  25  89  84   7   1  12   0   0   0
  22  94  29   9 120  32   0   0   1  21  43  69  81  20   0   0  22 120
  43  49  48 120  13   2  16  79  17   3  24   6   0   0  30  76  16   9
 120  64   7   5   5  10   7  38  64  75  36  37  38  54   5   8 109 120
   9   1   2   4  12  21  39  22   0   0  18  19   5   4 120 120  10   5
   1   0   0   0  27  42  44  52  37  20   6   2  24  10   3   2   7  42
  81  25]
[5]:
# Overlay keypoints on the image:
fig, ax = subplots()
ax.imshow(image)
ax.plot(keypoints[:].x, keypoints[:].y, ".g")
[5]:
[<matplotlib.lines.Line2D at 0x7fa7675242b0>]
[6]:
# Diplaying keypoints by scale:
fig, ax = subplots()
ax.hist(keypoints[:].scale, 100)
ax.set_xlabel("scale")
[6]:
Text(0.5,0,'scale')
[7]:
# One can see 3 groups of keypoints, boundaries at: 8 and 20. Let's display them using colors.
S = 8
L = 20
tiny = keypoints[keypoints[:].scale < S]
small = keypoints[numpy.logical_and(keypoints[:].scale < L, keypoints[:].scale >= S)]
bigger = keypoints[keypoints[:].scale >= L]

fig, ax = subplots()
ax.imshow(image, cmap="gray")
ax.plot(tiny[:].x, tiny[:].y, ",g", label="tiny")
ax.plot(small[:].x, small[:].y, ".b", label="small")
ax.plot(bigger[:].x, bigger[:].y, "or", label="large")
ax.legend()
[7]:
<matplotlib.legend.Legend at 0x7fa7487c0978>

Image matching and alignment#

Matching can also be performed on the device (GPU) as every single keypoint from an image needs to be compared with all keypoints from the second image.

In this simple example we will simple offset the first image by a few pixels

[8]:
shifted = numpy.zeros_like(image)
shifted[5:, 8:] = image[:-5, :-8]
shifted_points = sift_ocl(shifted)
[9]:
%time mp = sift.MatchPlan()
%time match = mp(keypoints, shifted_points)
print(
    "Number of Keypoints with for image 1 : %i, For image 2 : %i, Matching keypoints: %i"
    % (keypoints.size, shifted_points.size, match.shape[0])
)
from numpy import median

print(
    "Measured offsets dx: %.3f, dy: %.3f"
    % (median(match[:, 1].x - match[:, 0].x), median(match[:, 1].y - match[:, 0].y))
)
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 2.64 ms
CPU times: user 4 ms, sys: 0 ns, total: 4 ms
Wall time: 3.6 ms
Number of Keypoints with for image 1 : 411, For image 2 : 400, Matching keypoints: 353
Measured offsets dx: 8.000, dy: 5.000
[10]:
# Example of usage of the automatic alignment:
import scipy.ndimage

rotated = scipy.ndimage.rotate(image, 20, reshape=False)
sa = sift.LinearAlign(image, devicetype=devicetype)
fig, ax = subplots(1, 3, figsize=(9, 3))
ax[0].imshow(image)
ax[0].set_title("original")
ax[1].imshow(rotated)
ax[1].set_title("rotated")
ax[2].imshow(sa.align(rotated))
ax[2].set_title("aligned")
[10]:
Text(0.5,1,'aligned')

References#

[11]:
print("Total execution time: %.3fs" % (time.time() - start_time))
Total execution time: 1.972s