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

Finding the edge in a patch of pixels, and saying which way it points.

Mirrors `tbp/monty/frameworks/utils/edge_detection.py`. The two functions here
are the ones Monty keeps at module level; the detector itself is
`...EdgeDetection.EdgeDetector`, what it returns is
`...EdgeDetection.EdgeFeatures`, and the matrix it aggregates is
`...EdgeDetection.StructureTensor`.

## What an edge is here

A structure tensor over a patch of Sobel gradients, weighted towards the
centre of the patch and towards pixels where the gradient is strong. Two
numbers come out of it: how strong the edge is, and how *coherent* — one clean
direction against a wash of them. An edge is reported when both clear their
thresholds, which is why a uniform patch reports nothing at all rather than
reporting a weak edge in an arbitrary direction.

## The 2-D frame an edge implies

A surface agent moves on a curved surface, so a direction in the image is not
a direction on the object. `angle_to_pose_2d/4` projects the image direction
onto the local tangent plane and expresses it in that plane's frame, which is
the basis the 2-D model is built in. `TangentFrame` carries that basis, and it
is *transported* from step to step rather than recomputed, because a basis
that jumped at every step would make the model's coordinates jump with it.

## What this port supplies that Monty borrows

Monty reads pixels with OpenCV: `cvtColor`, `Sobel`, `GaussianBlur`. This port
has no OpenCV, so `...EdgeDetection.ImageOps` writes those three — the same
filters, over the same reflection at the patch border, which is what makes the
same patch give the same edge.

# `angle_to_pose_2d`

```elixir
@spec angle_to_pose_2d(
  float(),
  [[float()]],
  [float()],
  Monty.Frameworks.Utils.SpatialArithmetics.TangentFrame.t()
) :: Monty.Geometry.SurfacePoseVectors.t()
```

The 2-D pose an image-space edge angle implies, in the local tangent frame.

The image's `x` axis and its `y` axis (pointing *down* the image) are turned
into world directions by the camera's rotation, combined at `angle`, and
projected onto the tangent plane at `surface_normal`. What comes back is the
edge's direction in that plane as `[normal, edge_tangent, edge_perpendicular]`
— the normal always `[0, 0, 1]`, because this is the model's own 2-D frame,
not a direction in space.

A world direction that vanishes when projected (an edge seen edge-on) is not a
direction at all, so the frame's own first basis vector stands in.

# `default_surface_pose_vectors_2d`

```elixir
@spec default_surface_pose_vectors_2d() :: Monty.Geometry.SurfacePoseVectors.t()
```

The identity 2-D frame: normal `z`, tangent `x`, and `y` across it.

# `gradient_to_tangent_angle`

```elixir
@spec gradient_to_tangent_angle(float()) :: float()
```

Turns a gradient direction into an edge direction, wrapped to `[0, 2π)`.

An edge runs *across* its gradient, so a quarter turn. The wrap is what makes
the answer an orientation rather than a heading: a gradient pointing slightly
below the x-axis gives an edge just below 2π, not a negative angle.

---

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