Gabor filter banks for texture classification

In this example, we will see how to classify textures based on Gabor filter banks. Frequency and orientation representations of the Gabor filter are similar to those of the human visual system.

The images are filtered using the real parts of various different Gabor filter kernels. The mean and variance of the filtered images are then used as features for classification, which is based on the least squared error for simplicity.

../_images/plot_gabor_1.png

from __future__ import print_function

import matplotlib
import matplotlib.pyplot as plt
import numpy as np
from scipy import ndimage as nd

from skimage import data
from skimage.util import img_as_float
from skimage.filter import gabor_kernel


def compute_feats(image, kernels):
    feats = np.zeros((len(kernels), 2), dtype=np.double)
    for k, kernel in enumerate(kernels):
        filtered = nd.convolve(image, kernel, mode='wrap')
        feats[k, 0] = filtered.mean()
        feats[k, 1] = filtered.var()
    return feats


def match(feats, ref_feats):
    min_error = np.inf
    min_i = None
    for i in range(ref_feats.shape[0]):
        error = np.sum((feats - ref_feats[i, :])**2)
        if error < min_error:
            min_error = error
            min_i = i
    return min_i


# prepare filter bank kernels
kernels = []
for theta in range(4):
    theta = theta / 4. * np.pi
    for sigma in (1, 3):
        for frequency in (0.05, 0.25):
            kernel = np.real(gabor_kernel(frequency, theta=theta,
                                          sigma_x=sigma, sigma_y=sigma))
            kernels.append(kernel)


shrink = (slice(0, None, 3), slice(0, None, 3))
brick = img_as_float(data.load('brick.png'))[shrink]
grass = img_as_float(data.load('grass.png'))[shrink]
wall = img_as_float(data.load('rough-wall.png'))[shrink]
image_names = ('brick', 'grass', 'wall')
images = (brick, grass, wall)

# prepare reference features
ref_feats = np.zeros((3, len(kernels), 2), dtype=np.double)
ref_feats[0, :, :] = compute_feats(brick, kernels)
ref_feats[1, :, :] = compute_feats(grass, kernels)
ref_feats[2, :, :] = compute_feats(wall, kernels)

print('Rotated images matched against references using Gabor filter banks:')

print('original: brick, rotated: 30deg, match result: ', end='')
feats = compute_feats(nd.rotate(brick, angle=190, reshape=False), kernels)
print(image_names[match(feats, ref_feats)])

print('original: brick, rotated: 70deg, match result: ', end='')
feats = compute_feats(nd.rotate(brick, angle=70, reshape=False), kernels)
print(image_names[match(feats, ref_feats)])

print('original: grass, rotated: 145deg, match result: ', end='')
feats = compute_feats(nd.rotate(grass, angle=145, reshape=False), kernels)
print(image_names[match(feats, ref_feats)])


def power(image, kernel):
    # Normalize images for better comparison.
    image = (image - image.mean()) / image.std()
    return np.sqrt(nd.convolve(image, np.real(kernel), mode='wrap')**2 +
                   nd.convolve(image, np.imag(kernel), mode='wrap')**2)

# Plot a selection of the filter bank kernels and their responses.
results = []
kernel_params = []
for theta in (0, 1):
    theta = theta / 4. * np.pi
    for frequency in (0.1, 0.4):
        kernel = gabor_kernel(frequency, theta=theta)
        params = 'theta=%d,\nfrequency=%.2f' % (theta * 180 / np.pi, frequency)
        kernel_params.append(params)
        # Save kernel and the power image for each image
        results.append((kernel, [power(img, kernel) for img in images]))

fig, axes = plt.subplots(nrows=5, ncols=4, figsize=(5, 6))
plt.gray()

fig.suptitle('Image responses for Gabor filter kernels', fontsize=12)

axes[0][0].axis('off')

# Plot original images
for label, img, ax in zip(image_names, images, axes[0][1:]):
    ax.imshow(img)
    ax.set_title(label, fontsize=9)
    ax.axis('off')

for label, (kernel, powers), ax_row in zip(kernel_params, results, axes[1:]):
    # Plot Gabor kernel
    ax = ax_row[0]
    ax.imshow(np.real(kernel), interpolation='nearest')
    ax.set_ylabel(label, fontsize=7)
    ax.set_xticks([])
    ax.set_yticks([])

    # Plot Gabor responses with the contrast normalized for each filter
    vmin = np.min(powers)
    vmax = np.max(powers)
    for patch, ax in zip(powers, ax_row[1:]):
        ax.imshow(patch, vmin=vmin, vmax=vmax)
        ax.axis('off')

plt.show()

STDOUT


        

STDERR


        

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

IPython Notebook: download (generated using skimage 0.11dev)

ZnJvbSBfX2Z1dHVyZV9fIGltcG9ydCBwcmludF9mdW5jdGlvbgoKaW1wb3J0IG1hdHBsb3RsaWIKaW1wb3J0IG1hdHBsb3RsaWIucHlwbG90IGFzIHBsdAppbXBvcnQgbnVtcHkgYXMgbnAKZnJvbSBzY2lweSBpbXBvcnQgbmRpbWFnZSBhcyBuZAoKZnJvbSBza2ltYWdlIGltcG9ydCBkYXRhCmZyb20gc2tpbWFnZS51dGlsIGltcG9ydCBpbWdfYXNfZmxvYXQKZnJvbSBza2ltYWdlLmZpbHRlciBpbXBvcnQgZ2Fib3Jfa2VybmVsCgoKZGVmIGNvbXB1dGVfZmVhdHMoaW1hZ2UsIGtlcm5lbHMpOgogICAgZmVhdHMgPSBucC56ZXJvcygobGVuKGtlcm5lbHMpLCAyKSwgZHR5cGU9bnAuZG91YmxlKQogICAgZm9yIGssIGtlcm5lbCBpbiBlbnVtZXJhdGUoa2VybmVscyk6CiAgICAgICAgZmlsdGVyZWQgPSBuZC5jb252b2x2ZShpbWFnZSwga2VybmVsLCBtb2RlPSd3cmFwJykKICAgICAgICBmZWF0c1trLCAwXSA9IGZpbHRlcmVkLm1lYW4oKQogICAgICAgIGZlYXRzW2ssIDFdID0gZmlsdGVyZWQudmFyKCkKICAgIHJldHVybiBmZWF0cwoKCmRlZiBtYXRjaChmZWF0cywgcmVmX2ZlYXRzKToKICAgIG1pbl9lcnJvciA9IG5wLmluZgogICAgbWluX2kgPSBOb25lCiAgICBmb3IgaSBpbiByYW5nZShyZWZfZmVhdHMuc2hhcGVbMF0pOgogICAgICAgIGVycm9yID0gbnAuc3VtKChmZWF0cyAtIHJlZl9mZWF0c1tpLCA6XSkqKjIpCiAgICAgICAgaWYgZXJyb3IgPCBtaW5fZXJyb3I6CiAgICAgICAgICAgIG1pbl9lcnJvciA9IGVycm9yCiAgICAgICAgICAgIG1pbl9pID0gaQogICAgcmV0dXJuIG1pbl9pCgoKIyBwcmVwYXJlIGZpbHRlciBiYW5rIGtlcm5lbHMKa2VybmVscyA9IFtdCmZvciB0aGV0YSBpbiByYW5nZSg0KToKICAgIHRoZXRhID0gdGhldGEgLyA0LiAqIG5wLnBpCiAgICBmb3Igc2lnbWEgaW4gKDEsIDMpOgogICAgICAgIGZvciBmcmVxdWVuY3kgaW4gKDAuMDUsIDAuMjUpOgogICAgICAgICAgICBrZXJuZWwgPSBucC5yZWFsKGdhYm9yX2tlcm5lbChmcmVxdWVuY3ksIHRoZXRhPXRoZXRhLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzaWdtYV94PXNpZ21hLCBzaWdtYV95PXNpZ21hKSkKICAgICAgICAgICAga2VybmVscy5hcHBlbmQoa2VybmVsKQoKCnNocmluayA9IChzbGljZSgwLCBOb25lLCAzKSwgc2xpY2UoMCwgTm9uZSwgMykpCmJyaWNrID0gaW1nX2FzX2Zsb2F0KGRhdGEubG9hZCgnYnJpY2sucG5nJykpW3Nocmlua10KZ3Jhc3MgPSBpbWdfYXNfZmxvYXQoZGF0YS5sb2FkKCdncmFzcy5wbmcnKSlbc2hyaW5rXQp3YWxsID0gaW1nX2FzX2Zsb2F0KGRhdGEubG9hZCgncm91Z2gtd2FsbC5wbmcnKSlbc2hyaW5rXQppbWFnZV9uYW1lcyA9ICgnYnJpY2snLCAnZ3Jhc3MnLCAnd2FsbCcpCmltYWdlcyA9IChicmljaywgZ3Jhc3MsIHdhbGwpCgojIHByZXBhcmUgcmVmZXJlbmNlIGZlYXR1cmVzCnJlZl9mZWF0cyA9IG5wLnplcm9zKCgzLCBsZW4oa2VybmVscyksIDIpLCBkdHlwZT1ucC5kb3VibGUpCnJlZl9mZWF0c1swLCA6LCA6XSA9IGNvbXB1dGVfZmVhdHMoYnJpY2ssIGtlcm5lbHMpCnJlZl9mZWF0c1sxLCA6LCA6XSA9IGNvbXB1dGVfZmVhdHMoZ3Jhc3MsIGtlcm5lbHMpCnJlZl9mZWF0c1syLCA6LCA6XSA9IGNvbXB1dGVfZmVhdHMod2FsbCwga2VybmVscykKCnByaW50KCdSb3RhdGVkIGltYWdlcyBtYXRjaGVkIGFnYWluc3QgcmVmZXJlbmNlcyB1c2luZyBHYWJvciBmaWx0ZXIgYmFua3M6JykKCnByaW50KCdvcmlnaW5hbDogYnJpY2ssIHJvdGF0ZWQ6IDMwZGVnLCBtYXRjaCByZXN1bHQ6ICcsIGVuZD0nJykKZmVhdHMgPSBjb21wdXRlX2ZlYXRzKG5kLnJvdGF0ZShicmljaywgYW5nbGU9MTkwLCByZXNoYXBlPUZhbHNlKSwga2VybmVscykKcHJpbnQoaW1hZ2VfbmFtZXNbbWF0Y2goZmVhdHMsIHJlZl9mZWF0cyldKQoKcHJpbnQoJ29yaWdpbmFsOiBicmljaywgcm90YXRlZDogNzBkZWcsIG1hdGNoIHJlc3VsdDogJywgZW5kPScnKQpmZWF0cyA9IGNvbXB1dGVfZmVhdHMobmQucm90YXRlKGJyaWNrLCBhbmdsZT03MCwgcmVzaGFwZT1GYWxzZSksIGtlcm5lbHMpCnByaW50KGltYWdlX25hbWVzW21hdGNoKGZlYXRzLCByZWZfZmVhdHMpXSkKCnByaW50KCdvcmlnaW5hbDogZ3Jhc3MsIHJvdGF0ZWQ6IDE0NWRlZywgbWF0Y2ggcmVzdWx0OiAnLCBlbmQ9JycpCmZlYXRzID0gY29tcHV0ZV9mZWF0cyhuZC5yb3RhdGUoZ3Jhc3MsIGFuZ2xlPTE0NSwgcmVzaGFwZT1GYWxzZSksIGtlcm5lbHMpCnByaW50KGltYWdlX25hbWVzW21hdGNoKGZlYXRzLCByZWZfZmVhdHMpXSkKCgpkZWYgcG93ZXIoaW1hZ2UsIGtlcm5lbCk6CiAgICAjIE5vcm1hbGl6ZSBpbWFnZXMgZm9yIGJldHRlciBjb21wYXJpc29uLgogICAgaW1hZ2UgPSAoaW1hZ2UgLSBpbWFnZS5tZWFuKCkpIC8gaW1hZ2Uuc3RkKCkKICAgIHJldHVybiBucC5zcXJ0KG5kLmNvbnZvbHZlKGltYWdlLCBucC5yZWFsKGtlcm5lbCksIG1vZGU9J3dyYXAnKSoqMiArCiAgICAgICAgICAgICAgICAgICBuZC5jb252b2x2ZShpbWFnZSwgbnAuaW1hZyhrZXJuZWwpLCBtb2RlPSd3cmFwJykqKjIpCgojIFBsb3QgYSBzZWxlY3Rpb24gb2YgdGhlIGZpbHRlciBiYW5rIGtlcm5lbHMgYW5kIHRoZWlyIHJlc3BvbnNlcy4KcmVzdWx0cyA9IFtdCmtlcm5lbF9wYXJhbXMgPSBbXQpmb3IgdGhldGEgaW4gKDAsIDEpOgogICAgdGhldGEgPSB0aGV0YSAvIDQuICogbnAucGkKICAgIGZvciBmcmVxdWVuY3kgaW4gKDAuMSwgMC40KToKICAgICAgICBrZXJuZWwgPSBnYWJvcl9rZXJuZWwoZnJlcXVlbmN5LCB0aGV0YT10aGV0YSkKICAgICAgICBwYXJhbXMgPSAndGhldGE9JWQsXG5mcmVxdWVuY3k9JS4yZicgJSAodGhldGEgKiAxODAgLyBucC5waSwgZnJlcXVlbmN5KQogICAgICAgIGtlcm5lbF9wYXJhbXMuYXBwZW5kKHBhcmFtcykKICAgICAgICAjIFNhdmUga2VybmVsIGFuZCB0aGUgcG93ZXIgaW1hZ2UgZm9yIGVhY2ggaW1hZ2UKICAgICAgICByZXN1bHRzLmFwcGVuZCgoa2VybmVsLCBbcG93ZXIoaW1nLCBrZXJuZWwpIGZvciBpbWcgaW4gaW1hZ2VzXSkpCgpmaWcsIGF4ZXMgPSBwbHQuc3VicGxvdHMobnJvd3M9NSwgbmNvbHM9NCwgZmlnc2l6ZT0oNSwgNikpCnBsdC5ncmF5KCkKCmZpZy5zdXB0aXRsZSgnSW1hZ2UgcmVzcG9uc2VzIGZvciBHYWJvciBmaWx0ZXIga2VybmVscycsIGZvbnRzaXplPTEyKQoKYXhlc1swXVswXS5heGlzKCdvZmYnKQoKIyBQbG90IG9yaWdpbmFsIGltYWdlcwpmb3IgbGFiZWwsIGltZywgYXggaW4gemlwKGltYWdlX25hbWVzLCBpbWFnZXMsIGF4ZXNbMF1bMTpdKToKICAgIGF4Lmltc2hvdyhpbWcpCiAgICBheC5zZXRfdGl0bGUobGFiZWwsIGZvbnRzaXplPTkpCiAgICBheC5heGlzKCdvZmYnKQoKZm9yIGxhYmVsLCAoa2VybmVsLCBwb3dlcnMpLCBheF9yb3cgaW4gemlwKGtlcm5lbF9wYXJhbXMsIHJlc3VsdHMsIGF4ZXNbMTpdKToKICAgICMgUGxvdCBHYWJvciBrZXJuZWwKICAgIGF4ID0gYXhfcm93WzBdCiAgICBheC5pbXNob3cobnAucmVhbChrZXJuZWwpLCBpbnRlcnBvbGF0aW9uPSduZWFyZXN0JykKICAgIGF4LnNldF95bGFiZWwobGFiZWwsIGZvbnRzaXplPTcpCiAgICBheC5zZXRfeHRpY2tzKFtdKQogICAgYXguc2V0X3l0aWNrcyhbXSkKCiAgICAjIFBsb3QgR2Fib3IgcmVzcG9uc2VzIHdpdGggdGhlIGNvbnRyYXN0IG5vcm1hbGl6ZWQgZm9yIGVhY2ggZmlsdGVyCiAgICB2bWluID0gbnAubWluKHBvd2VycykKICAgIHZtYXggPSBucC5tYXgocG93ZXJzKQogICAgZm9yIHBhdGNoLCBheCBpbiB6aXAocG93ZXJzLCBheF9yb3dbMTpdKToKICAgICAgICBheC5pbXNob3cocGF0Y2gsIHZtaW49dm1pbiwgdm1heD12bWF4KQogICAgICAgIGF4LmF4aXMoJ29mZicpCgpwbHQuc2hvdygp