Monty.Frameworks.Utils.EdgeDetection.EdgeDetector (ExTbpMonty.Core v0.1.0)

Copy Markdown View Source

One patch in, one edge out.

Mirrors tbp/monty/frameworks/utils/edge_detection.py::EdgeDetector.

The detector does not ask "is there a gradient here" — a texture has gradients everywhere. It asks whether the gradients in the patch agree on one direction, which is what a structure tensor aggregated over the patch answers. Three things can then say no:

  • Nothing was seen. If every pixel in the weighted neighbourhood has no gradient at all (a flat patch), the aggregate is zero and there is no edge to report.
  • The edge is not in the middle. The weighted centroid of the gradient, measured along the edge's normal, is how far from the patch centre the edge actually runs. A patch that is half background has an edge at its rim, not through its centre, and a module that moved to the edge does not want to hear about a different edge at the rim.
  • It is too weak, or too muddled. strength_threshold and coherence_threshold are separate because a strong but incoherent patch (two textures crossing) is not an edge either.

Geometric or texture

An edge in brightness may be ink on a surface or the surface ending against the background. The depth image says which: across a geometric edge, depth jumps. Monty computes the depth gradient perpendicular to the brightness edge, inside a band around it, and calls the edge geometric when the 95th percentile of that gradient clears depth_edge_threshold. The percentile rather than the mean is what makes a one-pixel jump count without a wide crease being needed to prove it.

Summary

Functions

How far, in pixels, the edge runs from the patch centre — signed, along the edge's normal.

The weight of every pixel: how close it is to the centre, and how strong its gradient is.

Monty's __call__: the edge in this patch, or the reason there is none.

Whether the edge is a crease in the world rather than a change in ink.

A detector, as Monty's __init__ reads its arguments.

Types

t()

@type t() :: %Monty.Frameworks.Utils.EdgeDetection.EdgeDetector{
  coherence_threshold: float(),
  depth_edge_threshold: float(),
  gaussian_sigma: float(),
  kernel_size: pos_integer(),
  max_center_offset: number() | nil,
  radius: float(),
  sigma_r: float(),
  strength_threshold: float()
}

Functions

center_offset(weights, total_weight, normal_angle)

@spec center_offset([[number()]], float(), float()) :: float()

How far, in pixels, the edge runs from the patch centre — signed, along the edge's normal.

center_weights(detector, grayscale, grad_x, grad_y)

@spec center_weights(t(), [[number()]], [[number()]], [[number()]]) ::
  {[[float()]], float()}

The weight of every pixel: how close it is to the centre, and how strong its gradient is.

exp(-d² / 2σ_r²) with zero past radius, multiplied by Ix² + Iy². The first factor is what makes the patch answer for its middle; the second is what keeps a strong edge near the rim from being discarded just for being far away.

detect(detector, observation)

Monty's __call__: the edge in this patch, or the reason there is none.

The outline, in order: grey the patch, Sobel it, build a structure tensor per pixel and blur each component, weight the tensors towards the centre and towards strong gradients, aggregate, and then ask the three questions above.

geometric_edge?(detector, depth, edge_angle, center_offset)

@spec geometric_edge?(t(), [[float()]], float(), float()) :: boolean()

Whether the edge is a crease in the world rather than a change in ink.

The depth gradient across the edge, inside a band 1.5 pixels wide around where the brightness edge was found, and its 95th percentile against depth_edge_threshold.

new(options \\ [])

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

A detector, as Monty's __init__ reads its arguments.

:radius and :sigma_r shape the neighbourhood the tensor is aggregated over: a Gaussian that falls to nothing at radius pixels from the centre, and a falloff of sigma_r. :max_center_offset is the one setting that changes what "an edge" means — nil accepts an edge anywhere in the patch, and a number accepts it only that close to the centre.

An even :kernel_size is refused: a Gaussian of even size has no centre pixel, so the blur would be half a pixel off and the edge with it.