Introduction

Look at a brick wall. Without thinking, your brain instantly registers the horizontal lines of mortar, the vertical edges of each brick, the slight diagonal of a crack. That lightning-fast catalogue of orientations is not magic — it is a bank of Gabor filters firing in parallel.

A Gabor filter is the simplest possible "orientation detector": take a sine wave running in some direction at some spatial frequency, then multiply it by a Gaussian (a bell-shaped window) to keep only a local patch. The result is a rippled spotlight that responds strongly to edges or stripes aligned with its direction and tuned to its frequency, while suppressing everything else.

What makes Gabor filters remarkable is that they arise in two completely independent domains. In signal processing, they achieve the theoretical limit of joint time-frequency resolution — a consequence of the uncertainty principle. In neuroscience, electrophysiology recordings by Hubel and Wiesel in the 1960s showed that simple cells in the primary visual cortex (V1) have receptive fields shaped almost exactly like Gabor filters. Biology and mathematics converged on the same solution.

The two-dimensional Gabor filter in the spatial domain is

g(x,y;λ,θ,σ,γ)=exp⁡ ⁣(−x′2+γ2y′22σ2)cos⁡ ⁣(2πx′λ+φ)g(x, y; \lambda, \theta, \sigma, \gamma) = \exp\!\left(-\frac{x'^{2} + \gamma^{2} y'^{2}}{2\sigma^{2}}\right)\cos\!\left(\frac{2\pi x'}{\lambda} + \varphi\right)

where x′=xcos⁡θ+ysin⁡θx' = x\cos\theta + y\sin\theta and y′=−xsin⁡θ+ycos⁡θy' = -x\sin\theta + y\cos\theta rotate coordinates to the preferred orientation θ\theta, λ\lambda is the wavelength, σ\sigma controls the Gaussian width, γ\gamma is the spatial aspect ratio, and φ\varphi is a phase offset.

Try It: Filter Bank

Select a synthetic texture below and watch a bank of eight Gabor filters — spaced 22.5°22.5° apart — respond to it. Each bar in the chart shows how strongly that filter's orientation matches the dominant texture direction.

<!-- Gabor filter bank demo: pick a texture, watch which orientation wins -->
<p class="hint">Select a synthetic texture. A bank of 8 Gabor filters — covering 0° to 157.5° in steps of 22.5° — responds to it. The orange bar is the winning orientation; the filtered image shows that filter's response map.</p>
<div class="controls">
  <label for="tex-select">Texture:</label>
  <select id="tex-select">
    <option value="horizontal">Horizontal stripes</option>
    <option value="vertical">Vertical stripes</option>
    <option value="diagonal45">Diagonal stripes (45°)</option>
    <option value="diagonal135">Diagonal stripes (135°)</option>
    <option value="checker">Checkerboard</option>
    <option value="random">Random noise</option>
  </select>
</div>
<div class="canvases">
  <div>
    <div class="canvas-label">Texture patch</div>
    <canvas id="tex-canvas" width="120" height="120"></canvas>
  </div>
  <div>
    <div class="canvas-label">Winning filter response</div>
    <canvas id="resp-canvas" width="120" height="120"></canvas>
  </div>
</div>
<div class="chart-wrap">
  <div class="chart-label">Filter bank energy profile</div>
  <div id="chart"></div>
</div>
<div id="status" class="status"></div>
/* Gabor filter bank demo styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.controls { display: flex; align-items: center; gap: .6rem; margin-bottom: .8rem; flex-wrap: wrap; }
.controls label { font-size: .9rem; font-weight: 600; }
select { font: 14px system-ui; padding: .3rem .5rem; border: 1px solid #aaa; border-radius: 6px; cursor: pointer; }
.canvases { display: flex; gap: 1.2rem; margin-bottom: .9rem; flex-wrap: wrap; }
.canvas-label { font-size: .78rem; color: #555; margin-bottom: .3rem; text-align: center; }
canvas { border: 1px solid #cdd; border-radius: 6px; display: block; }
.chart-wrap { margin-bottom: .5rem; }
.chart-label { font-size: .82rem; color: #555; margin-bottom: .35rem; }
#chart { display: flex; align-items: flex-end; gap: 4px; height: 100px; }
.bar-wrap { display: flex; flex-direction: column; align-items: center; gap: 3px; flex: 1; }
.bar { width: 100%; background: #1d6fa5; border-radius: 4px 4px 0 0; transition: height .25s; min-height: 2px; }
.bar.winner { background: #e05c00; }
.bar-label { font-size: .7rem; color: #555; white-space: nowrap; }
.status { font-size: .92rem; font-weight: 600; min-height: 1.4em; color: #1d6fa5; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Build a bank of 8 Gabor filters at equally-spaced orientations and compute
// the RMS energy of each filter's response on the selected texture patch.

const NUM_FILTERS = 8; // orientations spaced PI/8 apart
const TEX_SIZE = 120; // texture canvas size in pixels
const KERNEL_SIZE = 31; // Gabor kernel side length (odd)
const SIGMA = 4.5; // Gaussian envelope width (pixels)
const LAMBDA = 9.0; // sinusoid wavelength (pixels)
const GAMMA = 0.5; // spatial aspect ratio (elongation)

// Build a real Gabor kernel for a given orientation theta (radians).
function buildGaborKernel(theta) {
  const half = Math.floor(KERNEL_SIZE / 2);
  const data = [];
  for (let y = -half; y <= half; y++) {
    for (let x = -half; x <= half; x++) {
      const xp = x * Math.cos(theta) + y * Math.sin(theta);
      const yp = -x * Math.sin(theta) + y * Math.cos(theta);
      const gauss = Math.exp(-(xp * xp + GAMMA * GAMMA * yp * yp) / (2 * SIGMA * SIGMA));
      const wave = Math.cos(2 * Math.PI * xp / LAMBDA);
      data.push(gauss * wave);
    }
  }
  return data;
}

// Generate a synthetic texture image as a Float32 array in [-1,1].
function buildTexture(type) {
  const px = new Float32Array(TEX_SIZE * TEX_SIZE);
  for (let y = 0; y < TEX_SIZE; y++) {
    for (let x = 0; x < TEX_SIZE; x++) {
      let v;
      if (type === 'horizontal') {
        v = Math.sin(2 * Math.PI * y / 10);
      } else if (type === 'vertical') {
        v = Math.sin(2 * Math.PI * x / 10);
      } else if (type === 'diagonal45') {
        v = Math.sin(2 * Math.PI * (x + y) / 14);
      } else if (type === 'diagonal135') {
        v = Math.sin(2 * Math.PI * (x - y) / 14);
      } else if (type === 'checker') {
        v = ((Math.floor(x / 10) + Math.floor(y / 10)) % 2 === 0) ? 1 : -1;
      } else {
        v = (Math.random() * 2 - 1);
      }
      px[y * TEX_SIZE + x] = v;
    }
  }
  return px;
}

// Convolve image (TEX_SIZE x TEX_SIZE) with kernel (KERNEL_SIZE x KERNEL_SIZE), zero-pad.
function convolve(img, kernel) {
  const half = Math.floor(KERNEL_SIZE / 2);
  const out = new Float32Array(TEX_SIZE * TEX_SIZE);
  for (let y = 0; y < TEX_SIZE; y++) {
    for (let x = 0; x < TEX_SIZE; x++) {
      let sum = 0;
      for (let ky = -half; ky <= half; ky++) {
        for (let kx = -half; kx <= half; kx++) {
          const iy = y + ky, ix = x + kx;
          if (iy >= 0 && iy < TEX_SIZE && ix >= 0 && ix < TEX_SIZE) {
            sum += img[iy * TEX_SIZE + ix] * kernel[(ky + half) * KERNEL_SIZE + (kx + half)];
          }
        }
      }
      out[y * TEX_SIZE + x] = sum;
    }
  }
  return out;
}

// Compute RMS energy of a filter response map.
function filterEnergy(response) {
  let e = 0;
  for (let i = 0; i < response.length; i++) e += response[i] * response[i];
  return Math.sqrt(e / response.length);
}

// Render a Float32 array as a grayscale ImageData on a canvas.
function drawGray(canvas, data) {
  const ctx = canvas.getContext('2d');
  const img = ctx.createImageData(TEX_SIZE, TEX_SIZE);
  let min = Infinity, max = -Infinity;
  for (let i = 0; i < data.length; i++) { if (data[i] < min) min = data[i]; if (data[i] > max) max = data[i]; }
  const range = max - min || 1;
  for (let i = 0; i < data.length; i++) {
    const v = Math.round(((data[i] - min) / range) * 255);
    img.data[i * 4] = v; img.data[i * 4 + 1] = v; img.data[i * 4 + 2] = v; img.data[i * 4 + 3] = 255;
  }
  ctx.putImageData(img, 0, 0);
}

// Pre-compute all kernels once at startup.
const kernels = [];
const angles = [];
for (let k = 0; k < NUM_FILTERS; k++) {
  const theta = k * Math.PI / NUM_FILTERS;
  angles.push(theta);
  kernels.push(buildGaborKernel(theta));
}

let currentTexture = 'horizontal';
let randomPx = null;

function run() {
  const type = document.getElementById('tex-select').value;
  if (type === 'random' && (currentTexture !== 'random' || randomPx === null)) {
    randomPx = buildTexture('random');
  }
  currentTexture = type;
  const px = (type === 'random') ? randomPx : buildTexture(type);

  drawGray(document.getElementById('tex-canvas'), px);

  // Convolve with each filter and collect RMS energies.
  const energies = kernels.map(k => filterEnergy(convolve(px, k)));
  const maxE = Math.max(...energies);

  // Identify the winning filter orientation.
  const winnerIdx = energies.indexOf(maxE);
  const winnerDeg = Math.round(angles[winnerIdx] * 180 / Math.PI);

  // Visualize the winning filter's response map.
  const respMap = convolve(px, kernels[winnerIdx]);
  drawGray(document.getElementById('resp-canvas'), respMap);

  // Render the energy bar chart.
  const chart = document.getElementById('chart');
  chart.innerHTML = '';
  energies.forEach((e, i) => {
    const deg = Math.round(angles[i] * 180 / Math.PI);
    const heightPct = maxE > 0 ? (e / maxE) * 100 : 0;
    const wrap = document.createElement('div');
    wrap.className = 'bar-wrap';
    const bar = document.createElement('div');
    bar.className = 'bar' + (i === winnerIdx ? ' winner' : '');
    bar.style.height = heightPct + '%';
    bar.title = i18nFmt('%deg%° — energy: %e%', { deg: deg, e: e.toFixed(3) });
    const lbl = document.createElement('div');
    lbl.className = 'bar-label';
    lbl.textContent = deg + '°';
    wrap.appendChild(bar);
    wrap.appendChild(lbl);
    chart.appendChild(wrap);
  });

  document.getElementById('status').textContent =
    i18nFmt('Dominant orientation: %deg%°', { deg: winnerDeg });
}

document.getElementById('tex-select').addEventListener('change', run);
run();
Result
Console

The winning bar is the filter whose sinusoid runs perpendicular to the edges in the texture — because edges are transitions across the filter's preferred direction. Change the texture and the whole energy profile shifts. That real-time competition among orientations is exactly what V1 runs on every patch of every image your eyes ever receive.

The Math and Biology

Why does this particular shape of filter keep appearing everywhere?

The uncertainty principle for images. Just as a quantum particle cannot have both a precise position and a precise momentum, a signal cannot be simultaneously localized in space and in frequency. The theoretical optimum — the filter that localizes both as tightly as possible — is a Gaussian-modulated sinusoid: exactly a Gabor filter. This was proved by Dennis Gabor in 1946 for 1D signals; the 2D generalization follows directly.

V1 simple cells match Gabor filters. Hubel and Wiesel (Nobel Prize, 1981) discovered that neurons in the primary visual cortex respond to oriented bars in a small patch of the visual field. Later work by Daugman (1985) and Jones & Palmer (1987) showed that the measured receptive fields of these neurons fit 2D Gabor functions with surprisingly small residual error. The brain did not stumble on this shape by accident — it is the mathematically optimal local feature detector.

A filter bank covers all orientations. No single orientation is privileged, so a practical system runs KK filters in parallel, with orientations θk=k⋅π/K\theta_k = k \cdot \pi / K for k=0,…,K−1k = 0, \dots, K-1. Typical choices are K=4K = 4 or K=8K = 8. For each spatial frequency band you repeat the same set, giving a complete multi-scale, multi-orientation filter bank.

The response is complex-valued. The filter above uses a cosine (even symmetry). Adding a sine counterpart (odd symmetry) gives a complex Gabor filter whose magnitude re2+ro2\sqrt{r_{e}^{2} + r_{o}^{2}} is phase-invariant — it reports how much energy is present at that orientation and scale, regardless of where exactly the edge falls within the receptive field. This is the quantity used in texture descriptors.

Gabor filters are closely related to pattern matching: the convolution of an image with a filter is mathematically a sliding dot product that peaks wherever the local image patch best resembles the filter kernel.

Where It Matters

Gabor filters sit at the intersection of neuroscience and engineering, and their applications span decades of computer vision:

  • Texture segmentation: Jain & Farrokhnia (1991) showed that a multi-channel Gabor filter bank followed by nonlinear processing reliably separates image regions by texture type — bricks from grass, stripes from spots.
  • Fingerprint verification: the ISO/IEC standard for minutiae extraction uses Gabor-based enhancement to sharpen ridge and valley structure before thinning, even on poor-quality prints.
  • Face recognition: the Gabor wavelets used in the Elastic Bunch Graph Matching system (Wiskott et al., 1997) extract 40 "jets" per landmark, matching faces across lighting and expression changes with state-of-the-art accuracy for its era.
  • Medical imaging: Gabor features detect micro-calcifications in mammograms and segment white matter tracts in MRI diffusion data, where local oriented structure is the signal of interest.
  • Convolutional neural networks: the filters learned in the first convolutional layer of trained CNNs (e.g., AlexNet) are visually almost identical to Gabor filters — the network independently rediscovers the same optimal edge detectors that evolution and Gabor theory both converged on.

Understanding Gabor filters gives you a direct window into dimensionality reduction: a filter bank projects each image patch onto a compact set of oriented-frequency coordinates, discarding the rest.

Conclusion

Gabor filters are a rare case where theory, biology, and engineering all agree. The uncertainty principle says the Gaussian-modulated sinusoid is the best possible local feature detector. Evolution built that very shape into V1 simple cells. And deep convolutional networks, trained with no knowledge of neuroscience, learn it again in their first layer.

The next time you glance at a textured surface and instantly know its orientation, remember: your visual cortex is running a filter bank tuned to dozens of orientations and spatial frequencies simultaneously — the same bank that powers fingerprint scanners, MRI analysis, and the opening layers of every major image recognition system. One elegant formula, many discoveries.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/gabor-filters/Content licensed under CC BY-NC 4.0.