# `Monty.Frameworks.Utils.EdgeDetection.EdgeDetector`
[🔗](https://github.com/d-led/ex.tbp.monty/blob/v0.1.0/packages/ex_tbp_monty_core/lib/monty/frameworks/utils/edge_detection/edge_detector.ex#L1)

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.

# `t`

```elixir
@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()
}
```

# `center_offset`

```elixir
@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`

```elixir
@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`

```elixir
@spec detect(t(), Monty.Observations.SensorObservation.t()) ::
  Monty.Frameworks.Utils.EdgeDetection.EdgeFeatures.t()
```

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?`

```elixir
@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`

```elixir
@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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
