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

The three OpenCV calls Monty's edge detector is built on, written out.

Monty reads pixels with `cv2.cvtColor`, `cv2.Sobel` and `cv2.GaussianBlur`.
This application has no OpenCV and no Python in the matching path, so the
three are here, with the properties that matter for an edge:

  * **Colour to grey** is OpenCV's fixed-point luma,
    `(4899 R + 9617 G + 1868 B + 8192) >> 14`, scaled to `[0, 1]`. The
    coefficients are the usual 0.299/0.587/0.114 in 14-bit fixed point, and the
    rounding term is what makes a half step round up rather than down.
  * **Sobel 3×3** is the separable `[-1, 0, 1] × [1, 2, 1]` derivative, with
    pixels outside the patch reflected **without** repeating the edge
    (`gfedcb | abcdefgh | gfedcba`) — OpenCV's `BORDER_REFLECT_101`, which is
    what `Sobel` and `GaussianBlur` default to. Reflecting rather than zeroing
    is why a patch's border does not read as a bright edge of its own.
  * **Gaussian blur** is a separable kernel of the requested size and sigma,
    centred on the pixel, normalised to sum to one, with the same reflection.

Pixels outside the patch matter more than they look: an image is a window onto
a surface that continues past it, and a filter that pretends otherwise invents
an edge at the window's rim.

# `gaussian_blur`

```elixir
@spec gaussian_blur([[number()]], pos_integer(), float()) :: [[float()]]
```

A symmetric Gaussian blur of the given kernel size and sigma.

Two one-dimensional passes, which is what OpenCV does: a `size × size` kernel
is the outer product of one vector of weights with itself, so the passes give
the same answer for `2 size` multiplications a pixel instead of `size²`.

# `gaussian_kernel`

```elixir
@spec gaussian_kernel(pos_integer(), float()) :: [float()]
```

A normalised Gaussian kernel of `size` weights, centred on the middle one.

# `percentile`

```elixir
@spec percentile([number()], float()) :: float()
```

The `percentile`-th value of `values`, interpolating between neighbours.

numpy's default, which is what Monty asks for: position `(n - 1) * p / 100` in
the sorted list, then a straight line between the two values it falls between.

# `reflected`

```elixir
@spec reflected(integer(), pos_integer()) :: non_neg_integer()
```

An index folded back into `0..(length - 1)` without repeating the edge.

`-1` reads `1` and `length` reads `length - 2` — OpenCV's `BORDER_REFLECT_101`.

# `sobel`

```elixir
@spec sobel([[number()]], :x | :y) :: [[float()]]
```

The derivative of an image along `:x` or `:y`, border reflected.

`:x` finds vertical edges (brightness changing left to right) and `:y`
horizontal ones — OpenCV's order, and the order every caller assumes.

# `to_gray`

```elixir
@spec to_gray([[number()]]) :: [[float()]]
```

The patch's brightness, in `[0, 1]`, from its red, green and blue channels.

Alpha is ignored: OpenCV's `COLOR_RGB2GRAY` reads three channels and this
reads the same three.

The fixed point is OpenCV's, which takes an 8-bit image. A world that renders
fractions of one instead — the stand-in scene does — is scaled here rather
than in every consumer: the two conventions meet once, at the fixed point that
only speaks the first.

---

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