The ImageFeatures package allows you to compute compact "descriptors" of images or image regions. These descriptors are in a form that permits comparison against similar descriptors in other images or other portions of the same image. This can be useful in many applications, such as object recognition, localization, or image registration.

ImagesFeatures has its own documentation, and you should consult that for a comprehensive overview of the functionality of the package. Here, we'll briefly illustrate one type of feature and its application to image registration, the BRISK descriptor.

The BRISK descriptor examines the structure of an image around a keypoint. Given a keypoint, the mean intensity (loosely-speaking) is computed in a set of patches surrounding the point:

BRISK Sampling Pattern

BRISK then re-represents these intensities in a way that is invariant under rotations. This allows you to compare descriptors in two images, one of which might be a rotated version of the other.

Let us take a look at a simple example where the BRISK descriptor is used to match two images where one has been translated by (50, 40) pixels and then rotated by an angle of 75 degrees. We will use the lighthouse image from the TestImages package for this example.

First, let us create the two images we will match using BRISK.

using ImageFeatures, TestImages, Images, ImageDraw, CoordinateTransformations

img = testimage("lighthouse")
img1 = Gray.(img)
rot = recenter(RotMatrix(5pi/6), [size(img1)...] .÷ 2)  # a rotation around the center
tform = rot ∘ Translation(-50, -40)
img2 = warp(img1, tform, indices(img1))
INFO: Could not find lighthouse in directory. Checking if it exists in the online repository.
INFO: Found lighthouse in the online repository. Downloading to the images directory.
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed

  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
100   145    0   145    0     0    914      0 --:--:-- --:--:-- --:--:--   917

100   156  100   156    0     0    607      0 --:--:-- --:--:-- --:--:--   607

 38  622k   38  239k    0     0   617k      0  0:00:01 --:--:--  0:00:01  617k
100  622k  100  622k    0     0  1568k      0 --:--:-- --:--:-- --:--:-- 41.5M

To calculate the descriptors, we first need to get the keypoints. For this tutorial, we will use the FAST corners to generate keypoints (see fastcorners).

features_1 = Features(fastcorners(img1, 12, 0.35))
features_2 = Features(fastcorners(img2, 12, 0.35))

To create the BRISK descriptor, we first need to define the parameters by calling the BRISK constructor.

brisk_params = BRISK()

Now pass the image with the keypoints and the parameters to the create_descriptor function.

desc_1, ret_features_1 = create_descriptor(img1, features_1, brisk_params)
desc_2, ret_features_2 = create_descriptor(img2, features_2, brisk_params)

The obtained descriptors can be used to find the matches between the two images using the match_keypoints function.

matches = match_keypoints(Keypoints(ret_features_1), Keypoints(ret_features_2), desc_1, desc_2, 0.1)

We can use the ImageDraw.jl package to view the results.

grid = hcat(img1, img2)
offset = CartesianIndex(0, size(img1, 2))
map(m -> draw!(grid, LineSegment(m[1], m[2] + offset)), matches)
WARNING: one(ColorTypes.Gray{FixedPointNumbers.Normed{UInt8,8}}) will soon switch to returning 1; you might need to switch to `oneunit`
 [1] depwarn(::String, ::Symbol) at ./deprecated.jl:70
 [2] one(::Type{ColorTypes.Gray{FixedPointNumbers.Normed{UInt8,8}}}) at /home/travis/.julia/v0.6/ColorTypes/src/traits.jl:341
 [3] draw! at /home/travis/.julia/v0.6/ImageDraw/src/line2d.jl:64 [inlined] (repeats 2 times)
 [4] _collect(::Array{Array{CartesianIndex{2},1},1}, ::Base.Generator{Array{Array{CartesianIndex{2},1},1},ex-3.##3#4}, ::Base.EltypeUnknown, ::Base.HasShape) at ./array.jl:488
 [5] map(::Function, ::Array{Array{CartesianIndex{2},1},1}) at ./abstractarray.jl:1868
 [6] eval(::Module, ::Any) at ./boot.jl:235
 [7] cd(::Documenter.Expanders.##8#10, ::String) at ./file.jl:70
 [8] withoutput(::Documenter.Expanders.##7#9{Documenter.Documents.Page}) at /home/travis/.julia/v0.6/Documenter/src/Utilities/Utilities.jl:557
 [9] runner(::Type{Documenter.Expanders.ExampleBlocks}, ::Base.Markdown.Code, ::Documenter.Documents.Page, ::Documenter.Documents.Document) at /home/travis/.julia/v0.6/Documenter/src/Expanders.jl:459
 [10] dispatch(::Type{Documenter.Expanders.ExpanderPipeline}, ::Base.Markdown.Code, ::Vararg{Any,N} where N) at /home/travis/.julia/v0.6/Documenter/src/Selectors.jl:168
 [11] expand(::Documenter.Documents.Document) at /home/travis/.julia/v0.6/Documenter/src/Expanders.jl:31
 [12] runner(::Type{Documenter.Builder.ExpandTemplates}, ::Documenter.Documents.Document) at /home/travis/.julia/v0.6/Documenter/src/Builder.jl:178
 [13] dispatch(::Type{Documenter.Builder.DocumentPipeline}, ::Documenter.Documents.Document, ::Vararg{Documenter.Documents.Document,N} where N) at /home/travis/.julia/v0.6/Documenter/src/Selectors.jl:168
 [14] cd(::Documenter.##2#3{Documenter.Documents.Document}, ::String) at ./file.jl:70
 [15] #makedocs#1(::Bool, ::Array{Any,1}, ::Function) at /home/travis/.julia/v0.6/Documenter/src/Documenter.jl:204
 [16] (::Documenter.#kw##makedocs)(::Array{Any,1}, ::Documenter.#makedocs) at ./<missing>:0
 [17] include_from_node1(::String) at ./loading.jl:576
 [18] include(::String) at ./sysimg.jl:14
 [19] eval(::Module, ::Any) at ./boot.jl:235
 [20] process_options(::Base.JLOptions) at ./client.jl:286
 [21] _start() at ./client.jl:371
while loading /home/travis/build/JuliaImages/, in expression starting on line 3

You can see that the points have been accurately matched despite the large magnitude of this rotation.