Monty.Frameworks.Models.Salience.Strategies.Vocus2.OrientationSalience (ExTbpMonty.Core v0.1.0)

Copy Markdown View Source

How much a patch looks like an edge, at several orientations.

Mirrors tbp/monty/frameworks/models/salience/strategies/vocus2/vocus2.py::OrientationSalience.

Four Gabor filters at 0°, 45°, 90° and 135° are run over the detail of the lightness pyramid, and the absolute response is kept. A Gabor filter answers strongly where the image has structure at its wavelength, so "this patch is an edge" becomes a number — and the four orientations together make an edge salient whichever way it runs. Filtering the Laplacian rather than the image keeps a response local to the scale it was found at.

Each kernel has its mean subtracted before use, so a large flat region answers nothing instead of answering in proportion to its brightness: the filter is meant to detect structure, not light.

Measured, not recalled

cv2.getGaborKernel is not a dependency of the port, so both of its halves were measured against OpenCV 4.10 and reproduced: the Gaussian envelope with sigma_y = sigma / gamma, the rotation x_r = x cos θ + y sin θ and y_r = -x sin θ + y cos θ, and — the part that is easy to get wrong — the modulation entering as cos(2π x_r / λ - ψ), with a minus psi. A plus psi agrees when psi is zero and is wrong for every other phase, which is how the mistake survived a first reading.

Filtering is correlation, not convolution, with OpenCV's default BORDER_REFLECT_101; measured to 1.0e-6 on random images.

Summary

Functions

Correlates a plane with a kernel, as cv2.filter2D does.

The Gabor kernels: one per orientation, each centred on zero mean.

Orientation salience from :n_orientations Gabor filters of :period.

The orientation salience map for one pyramid of lightness.

Types

t()

@type t() :: %Monty.Frameworks.Models.Salience.Strategies.Vocus2.OrientationSalience{
  collapse:
    (Monty.Frameworks.Models.Salience.Strategies.Vocus2.Pyramids.Pyramid.t() ->
       [[float()]]),
  combine:
    (Monty.Frameworks.Models.Salience.Strategies.Vocus2.MapCombine.maps() ->
       [[float()]]),
  gamma: float(),
  kernels: %{optional(String.t()) => [[float()]]},
  n_orientations: pos_integer(),
  period: float(),
  phase: float(),
  sigma: float()
}

Functions

correlate(plane, kernel)

@spec correlate([[float()]], [[float()]]) :: [[float()]]

Correlates a plane with a kernel, as cv2.filter2D does.

The kernel is laid over the pixel unflipped — correlation, not convolution — and the border is reflected without repeating the edge pixel (BORDER_REFLECT_101), so [a b c d] continues [c b | a b c d | c b].

The padded plane and the kernel are turned into tuples first, because every pixel reads the same neighbourhood many times and a list is a walk from the front each time.

kernels(options)

@spec kernels(keyword()) :: %{optional(String.t()) => [[float()]]}

The Gabor kernels: one per orientation, each centred on zero mean.

The kernel is int(7*sigma + 1) | 1 pixels square — Monty's arithmetic, and OpenCV's rule for a kernel size it is not given.

new(options)

@spec new(keyword()) :: t()

Orientation salience from :n_orientations Gabor filters of :period.

:sigma defaults to 0.3 * period, which is Monty's rule of thumb, and :phase to a quarter turn, which finds edges; a phase of zero finds strips.

process(salience, context, pyramid)

The orientation salience map for one pyramid of lightness.

Each orientation answers one map — the detail of the pyramid filtered and rectified — and the maps are combined by :combine, whose default is their mean.