# Detection tuning

Out of the box, Fregata runs a bundled YOLOv9-tiny model at 320×320
on the Apple Neural Engine. This is fast (~1 ms per frame on Apple M4),
private (it's an ONNX file inside the app bundle, no cloud), and
"good enough" for porches, driveways, and most outdoor uses. This
page covers everything you'd reach for when "good enough" isn't good enough.

## What's running, by default

The defaults that ship with the app:

```yaml
detectors:
  coreml:
    type: coreml
    inference_backend: ane     # "ane" | "gpu"

model:
  model_type: yolo-generic
  width: 320
  height: 320
  input_tensor: nchw
  input_pixel_format: rgb
  input_dtype: float
```

Translated:

- **`type: coreml`** — the only detector type that ships in Fregata.
  No EdgeTPU, TensorRT, OpenVINO, ROCm, RKNN, Hailo. Those don't
  apply on macOS. (For the full list of what's removed, see
  [Fregata vs Frigate](/reference/fregata-vs-frigate/).)
- **`inference_backend: ane`** — route inference through ONNX
  Runtime's CoreML execution provider preferring the ANE. Fall through to the GPU or CPU only when ops
  aren't supported. Switch to `gpu` to force GPU inference.
- **`model_type: yolo-generic`** — generic YOLO postprocessor.
  Other supported types: `yolox`, `yolonas`, `dfine`, `rfdetr`.

## Detection resolution and FPS

Two `detect:` config keys control **what frames the detector
sees** — separately from what the camera streams and what
Fregata records. They're worth setting explicitly even though
they have defaults.

### `detect.width` and `detect.height` — match your main stream

By default Frigate probes the first frame of your camera's
stream and uses that resolution for detection — and Fregata
runs detection on the **main, full-resolution stream**, not a
low-resolution sub-stream the way other NVRs typically do. The
ANE has the headroom for it, and detection on the full frame
catches small or distant objects that a 1280 × 720 sub-stream
would pixel-soup before the detector sees them.

If the auto-probe doesn't pick up your camera's resolution
(some firmwares lie about frame size, some streams take a long
time to publish their first I-frame), Frigate falls back to
`1280 × 720`. **Set `detect.width` and `detect.height`
explicitly to your camera's main-stream resolution** to avoid
ever hitting that fallback:

```yaml
cameras:
  driveway:
    detect:
      width:  3840    # camera's main-stream width
      height: 2160    # camera's main-stream height
      fps:    10
```

Find your camera's actual main-stream resolution in the
camera's own web UI (Reolink, Amcrest, Dahua, etc. all surface
it under stream settings) or via `ffprobe`:

```bash
ffprobe -v error -select_streams v:0 \
  -show_entries stream=width,height \
  -of csv=p=0 rtsp://user:pass@camera-ip/stream
```

The part of the frame with motion is automatically cropped to the detector model's
input size (320 × 320 for the bundled YOLOv9-tiny) before
inference — those are the `model.width` and `model.height`
keys above, and changing them is a different operation (see
[Bringing your own model](#bringing-your-own-model)).

### `detect.fps` — don't go above 10

`detect.fps` controls **how many frames per second per camera**
the detector processes. It's independent of the camera's own
stream FPS and of the recording stream (which always captures
at the camera's native rate for playback).

:::note[Don't lower the camera's FPS — in the recommended no-sub-stream setup]
This is the single most common configuration mistake on Fregata
installs in the **default no-sub-stream mode** (one stream per
camera, used for both detection and recording). In that mode:
**keep the camera streaming at its native FPS** (15, 25, 30,
whatever it produces by default) — that's what gets recorded
for playback. Only tune **`detect.fps` in `config.yml`** to
control how often the detector runs. Lowering FPS in the
camera's own settings would also drops the recording quality.

**If you've configured a separate low-resolution detection
sub-stream** (less common on Fregata, since the ANE handles
full-resolution detection — but supported), the *sub-stream's*
own FPS in the camera firmware is the right place to lower the
rate that hits the detector; that's the whole point of running
a sub-stream. Keep the **main / recording stream** at native
FPS regardless.
:::

**Don't set `detect.fps` above 10**, even though Fregata's ANE
has the headroom. Reasons:

- **No detection-accuracy benefit.** Object-detection is
  per-frame. Detecting at 30 FPS doesn't catch
  more objects than detecting at 10 FPS — it just classifies
  the same objects 3× as many times.
- **Frigate's config validator warns above 10.** Every camera
  with `detect.fps > 10` (and `type` other than `lpr`)
  triggers `"Recommended value is 5"` in the Frigate log on
  startup. Going higher works but the validator is right: the
  work is wasted.
- **It's real ANE cycles and heat** that you don't get back.

5 FPS is plenty for most scenes; 10 FPS makes sense
for fast-moving subjects (cars driving by a busy street, other fast moving objects). Pick the lowest value that
catches what you need.

## ANE vs GPU — which should I use?

The honest answer: **leave it on `ane` and don't think about it
again** for the bundled model and the typical Frigate+ models. The
ANE is the faster path on Apple Silicon for INT8 / FP16 YOLO-shaped
networks, and the runtime falls back automatically when an op isn't
supported. The GPU will be slower per frame, use more electricity, and generate more heat.

Switch to `inference_backend: gpu` when:

- You're running a model the CoreML compiler can't lower onto the
  ANE (you'll see this on first warmup as a CPU-tier latency).
- You want to A/B test latency or thermals on a specific machine.
- You're hitting a known ANE bug on a specific macOS build and need
  to ship a fix today.

On an M4, ANE inference for YOLOv9-tiny at 320×320 is
~1 ms; GPU is 4–8 ms; CPU is 40–80 ms. The CPU path shouldn't be used — never run a real install on it.

On an M4, ANE inference for YOLOv9-small at 320×320, such as a Frigate+ model is ~2ms

## Masks

Masks blank out parts of the frame **before motion detection runs** —
they're narrow tools for fine-tuning, not for hiding an area from
Frigate. Use a motion mask for areas that obviously aren't an object of
interest: tree branches, the camera timestamp, a flag that waves all
day. Use `objects.filters.<class>.mask` to suppress detections of one
class only — handy for "ignore the person on the TV or reflected in the
window" scenarios.

**Don't reach for a mask to hide an area you just don't want alerts
about** — your neighbor's front porch, the sidewalk, a public street.
Over-masking degrades tracking: an object that walks from an unmasked
area into a masked one disappears and gets picked up as a "new" object
if it re-emerges, which is exactly the kind of false negative you don't
want. The right tool for "stop detecting/tracking activity here, but
don't alert on it" is a [zone](https://docs.frigate.video/configuration/zones)
combined with `review.alerts.required_zones` (and/or
`review.detections.required_zones`) — Frigate keeps tracking the object,
it just won't create a review item until the object enters a required
zone.

The web UI has a polygon editor under
**Settings → Masks & Zones**. Draw, save, repeat.

The full reference (coordinate format, multi-polygon syntax,
interactions with motion-detection sensitivity) is upstream — see
[Frigate's masks documentation](https://docs.frigate.video/configuration/masks).
The schema works on macOS unchanged.

## Zones

Zones are named polygons. They don't change *whether* an object is
detected — they change what events that detection creates and what
gets sent to MQTT or Home Assistant.

```yaml
cameras:
  driveway:
    zones:
      driveway_apron:
        coordinates: 0,720,400,720,500,500,0,500
        objects:
          - car
          - truck
      mailbox:
        coordinates: 1100,400,1280,400,1280,500,1100,500
```

A `car` event in `driveway_apron` will fire as a zone-entry event.
A `person` walking past the mailbox won't, because `mailbox` doesn't
list `person`.

This is the same configuration shape upstream Frigate uses; their
[zones documentation](https://docs.frigate.video/configuration/zones)
covers more advanced shapes.

## Per-object thresholds

The bundled YOLO model returns a confidence score for every box. A
sensible default is to ignore anything under ~0.5, and to be stricter
(~0.7) for classes that are easy to confuse with similar objects:

```yaml
objects:
  track:
    - person
    - car
    - dog
  filters:
    person:
      threshold: 0.7
      min_area: 1500
    dog:
      threshold: 0.65
```

`min_area` is in pixels; it kills tiny detections — usually
distant people that hover at low confidence.

## Bringing your own model

Fregata supports any ONNX model that ONNX Runtime's CoreML provider
can run. The most common reasons to swap:

1. **You bought a Frigate+ subscription** and want to use your
   custom-trained model. See below.
2. **You've trained YOLOv9 / YOLOv10 / RT-DETR yourself** and have
   an `onnx` export. Drop it in.
3. **You want classes the bundled model doesn't have** — e.g.
   bicycles, packages, license plates, drones.

Place the ONNX file somewhere persistent (the conventional spot is
`~/Fregata/config/models/my_model.onnx`) and point the config at it:

```yaml
detectors:
  coreml:
    type: coreml
    inference_backend: ane

model:
  path: /Users/<you>/Fregata/config/models/my_model.onnx
  model_type: yolo-generic   # or yolox, yolonas, dfine, rfdetr, ssd
  width: 320
  height: 320
  input_tensor: nchw
  input_pixel_format: rgb
  input_dtype: float
  labelmap_path: /Users/<you>/Fregata/config/models/labels.txt
```

The `labelmap_path` is a plain text file with one class label per
line, matching the model's output indices. If you skip it, Fregata
falls back to the COCO label map that ships with the app.

### Frigate+

Frigate+ models work natively. You'll see them on your account
dashboard with a `plus://...` identifier; once you've added your
Frigate+ API key, your `config.yml` will update automatically to
point at it via `model.path`:

```yaml
detectors:
  coreml:
    type: coreml

model:
  path: plus://abc123def456
```

Fregata fetches the model into `~/Fregata/config/model_cache/` on
first launch and validates the architecture. It checks the model's
declared `supportedDetectors` field for `onnx`; if your Frigate+ model is older than the
`onnx`-support cutover, retrain on the dashboard for free.

:::note
Frigate+ training and account management lives at
[frigate.video/plus](https://frigate.video/plus). Fregata doesn't
resell or proxy it — your subscription is directly with the
Frigate project. Fregata just consumes the resulting model file.
:::

## Verifying a model swap worked

After a config reload (or `Restart Frigate` from the tray):

1. Watch the **Detector** row in the tray. The first inference logs
   a warmup-tier classification — `ANE`, `GPU`, or `CPU`. CPU after
   a model swap usually means an unsupported op.
2. Open the web UI's **System** tab. The "Detector inference time"
   chart should plateau within a few seconds at the same tier.
3. Send an obvious test through (walk past the camera). If the
   bounding box is centered on the right object, you're done.

If the inference time has jumped from ~2 ms to 50+ ms, you've
fallen back to CPU. Either set `inference_backend: gpu` or
re-export the model with op set ≤ 17 — see
[Troubleshooting](/guides/troubleshooting/).
