# 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.

## Why inference speed changes on a quiet Mac

Apple Silicon doesn't run at one fixed speed. macOS continuously scales
the clocks of the CPU clusters **and** the Neural Engine to match
demand — when a Mac has nothing much to do, it drops its cores to a low
frequency to save power. That isn't a Fregata setting, and it isn't
something Fregata can turn off — macOS exposes no public API to pin a
clock floor.

So on a very quiet Mac — one camera, little motion, nothing else
running — an individual inference can take **3 ms or more** where the
same hardware delivers **~1 ms** once it's clocked up, drifting up and
down on a multi-second cycle instead of settling. Put any real load on
the CPU and it drops straight back to its floor. The hardware is fine;
it's just idling.

### This costs you nothing

The reassuring part is structural. Fregata doesn't run the detector on
every frame at a fixed rate — `detect.fps` sets how often frames are
*examined*, but the model only runs on the parts of a frame where motion
or an already-tracked object appears. Point a camera at a still scene
and it runs **zero** detections; the speed you see is simply the last
one it did.

So the slow readings and the busy moments never actually overlap.
Inference only drifts upward **because** nothing is happening — and the
instant something does (a person walks in, a car pulls up), that same
activity is what pulls the clocks back up. You never pay the throttled
rate on a frame that matters, because a frame that matters is one where
the Mac is already working.

There's also plenty of headroom. A single detector, shared across all of
your cameras, handles each region in a couple of milliseconds — fast
enough to keep pace with every camera you're realistically going to run.
Whatever the clock speed, detection accuracy is identical; the frequency
only ever changed *how quickly the number was reported*, never *what was
found*.

### What makes the reported number faster

Counter-intuitively, a single camera on an otherwise-idle Mac is the
**slowest** case for the reported figure — even though it's the lightest
possible workload. The number *improves* as you ask the Mac for more:

- **More cameras.** Each one's decode and motion work keeps cores busy.
  Measured on M4 machines with the bundled model: **~3.2 ms** with one
  camera on an otherwise-idle Mac, **~2.8 ms** with two, and **~1.1 ms**
  with six.
- **More frequent motion.** More active detection means more sustained
  load, and more sustained load means higher sustained clocks.
- **Other apps running** on the Mac. Anything that keeps the machine at
  higher clock speeds does the same job.

## 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,1,0.31,1,0.39,0.69,0,0.69
        objects:
          - car
          - truck
      mailbox:
        coordinates: 0.86,0.56,1,0.56,1,0.69,0.86,0.69
```

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

Coordinates are **relative** — `x,y` pairs expressed as fractions of
the frame between `0` and `1`. This is what the web UI's polygon editor
writes, and it's what you want: the zone follows the frame whatever you
set `detect.width`/`detect.height` to. Frigate also accepts legacy
pixel coordinates (any value above `1`), but it interprets them against
your *detect* resolution — so a polygon drawn for a 1280 × 720 frame
collapses into the top-left corner once you set `detect.width: 3840` as
recommended above. Stick to relative coordinates.

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, and
Frigate filters on it with **two separate keys** per class — worth
keeping straight, because they do different jobs:

- **`min_score`** (default `0.5`) — the floor for an *individual*
  detection. Any single box scoring below it is thrown away.
- **`threshold`** (default `0.7`) — the *median* score across all the
  frames an object has been tracked in, before it counts as a real
  object at all.

The defaults are sensible, so only reach for these when a specific
class misbehaves. Raise `threshold` **above `0.7`** for classes that
are easy to confuse with similar objects:

```yaml
objects:
  track:
    - person
    - car
    - dog
  filters:
    person:
      min_area: 1500
    dog:
      threshold: 0.8
      min_score: 0.6
```

Here `dog` is tightened on both counts — dogs get confused with other
animals, so individual boxes need to clear `0.6` and the tracked object
needs a median of `0.8`. `person` is left at the default confidence and
only gains a size filter.

Note that setting a value *equal* to the default does nothing, and
setting `threshold` *below* `0.7` makes that class **more** permissive,
not less — an easy way to accidentally invite the false positives you
were trying to remove.

`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/).
