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
Section titled “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
Section titled “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
Section titled “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
Section titled “What’s running, by default”The defaults that ship with the app:
- Settings → Fregata (macOS) → Global configuration → Apple Silicon Detector shows the detector and its Inference backend (Apple Neural Engine (recommended) by default).
- Settings → System → Detectors and model shows the Detector
Hardware and the Detection Model. On a default install,
Custom object detector model path shows the bundled model’s path
(it ends in
models/default.onnx, inside Fregata.app). With a Frigate+ API key set, the card has Frigate+ and Custom Model tabs, and the model fields are on Custom Model.
You only need to open these pages to change something; see the sections below.
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: floatTranslated:
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.)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 togputo force GPU inference.model_type: yolo-generic— generic YOLO postprocessor. Other supported types:yolox,yolonas,dfine,rfdetr.
Detection resolution and FPS
Section titled “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
Section titled “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:
- In the web UI, go to Settings → Camera configuration → Object detection and pick the camera in the selector at the top.
- Set Detect width and Detect height to the camera’s main-stream
resolution (for example
3840and2160). - Set Detect FPS (see below).
- Click Save, then Restart when the page asks. These three fields always need a restart.
To use the same values for every camera, use Settings → Global configuration → Object detection instead. That only makes sense when all your cameras share one resolution and aspect ratio.
cameras: driveway: detect: width: 3840 # camera's main-stream width height: 2160 # camera's main-stream height fps: 10A camera you add through the Add Camera wizard gets these two values filled in for you, from the resolution the wizard measured on its detect stream. Cameras the wizard added in Fregata v0.18.0 were capped at 720 pixels on the shorter side (a 3840 × 2160 stream was saved as 1280 × 720). Raise them as above, then reset the region grid.
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:
ffprobe -v error -select_streams v:0 \ -show_entries stream=width,height \ -of csv=p=0 rtsp://user:pass@camera-ip/streamThe 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).
Raised detect.width/detect.height? Reset the region grid
Section titled “Raised detect.width/detect.height? Reset the region grid”Frigate doesn’t crop every frame the same way. Per camera, it keeps
an 8 × 8 grid of “expected crop size” — one entry per zone of the
frame, learned from the actual size of objects it has detected there
— so it can draw a tight region around a person on the sidewalk
instead of guessing from scratch on every frame. It’s cached in
frigate.db, in a regions table, one row per camera.
That cache is never wiped, only appended to. Raise a camera’s
detect.width/detect.height (swap a 640 × 480 sub-stream for the
4K main stream, say) and the pre-existing entries — sized for the old,
lower resolution, and mostly pinned at the detector’s minimum crop
size because of it — stay mixed into the same running average
indefinitely. Regions keep coming back sized for the resolution you
left, not the one you’re on, and the small or distant objects the
higher resolution should now resolve keep getting missed. Nothing
about this self-corrects; the stale entries never age out.
Clear it from the web UI: Settings → Maintenance → Region grid, pick the camera from the camera selector at the top of the page, and click Clear region grid. The page shows the live 8 × 8 grid over a snapshot, so you can watch it empty out. Frigate needs to restart to rebuild it — the page flags this after clearing; use Restart Frigate from the Fregata tray.
Nothing is lost — on restart Frigate rebuilds the grid from that
camera’s own past detections, replayed against the resolution
config.yml now specifies.
detect.fps — don’t go above 10
Section titled “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).
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(andtypeother thanlpr) 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.
- Go to Settings → Camera configuration → Object detection and pick the camera.
- Set Detect FPS to
5(or10for fast-moving subjects). - Click Save, then Restart when the page asks.
cameras: driveway: detect: fps: 5ANE vs GPU — which should I use?
Section titled “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.
- In the web UI, go to Settings → Fregata (macOS) → Global configuration → Apple Silicon Detector.
- Set Inference backend to Metal GPU (or back to Apple Neural Engine (recommended)).
- Click Save, then Restart when the page asks.
After the restart, the same page shows the Resolved tier, Compute units, and Inference speed the detector actually ended up with.
detectors: coreml: type: coreml inference_backend: gpu # back to the default with: aneOn 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 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
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.
- In the web UI, go to Settings → Camera configuration → Masks / Zones and pick the camera in the selector at the top.
- For a motion mask, click the + next to Motion Mask (its tooltip reads New Motion Mask). For an object filter mask, click the + next to Object Masks (Add Object Mask) and choose the object type.
- Click on the camera image to draw the polygon, and give the mask an optional name.
- Click Save. Masks apply as soon as you save; no restart is needed.
Each mask has an Enabled switch, so you can turn one off without deleting it.
Coordinates are relative (fractions of the frame between 0 and 1),
which is what the polygon editor writes. Write the frame edge as 1, not
1.000: Frigate reads any value that sorts above the text 1.0 as pixel
coordinates.
cameras: driveway: motion: mask: timestamp: # any unique name friendly_name: "Timestamp" enabled: true coordinates: "0,0,0.3,0,0.3,0.06,0,0.06" objects: filters: person: mask: roof: friendly_name: "Roof area" enabled: true coordinates: "0,0,1,0,1,0.4,0,0.4"The full reference (coordinate format, multi-polygon syntax, interactions with motion-detection sensitivity) is upstream — see Frigate’s masks documentation. The schema works on macOS unchanged.
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.
- In the web UI, go to Settings → Camera configuration → Masks / Zones and pick the camera.
- Click the + next to Zones (its tooltip reads Add Zone), enter a Name (at least two characters, with a letter in it, and not the name of a camera), and click on the image to draw the polygon.
- Under Objects, choose which object types this zone applies to. Leave it on All Objects to apply to every tracked object.
- Click Save. Zones apply as soon as you save; no restart is needed.
cameras: driveway: zones: driveway_apron: coordinates: 0,1,0.31,1,0.39,0.69,0,0.69 objects: - car mailbox: coordinates: 0.86,0.56,1,0.56,1,0.69,0.86,0.69A car event in driveway_apron will fire as a zone-entry event, and
only a car: a zone with an objects list only reacts to the objects
it names. mailbox has no objects list, so any tracked object that
enters it counts. Every object a zone names must also be in
objects.track, or Frigate refuses the config.
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 covers more advanced shapes.
To keep alerts to the places you care about (instead of masking the rest of the frame, see Masks), require a zone before an object creates a review item:
- In the web UI, go to Settings → Camera configuration → Review and pick the camera. The camera needs at least one zone (see above).
- In the Review Classification block, use Select zones for Alerts to choose the zones an object must enter to be an alert. Select zones for Detections does the same for detections.
- Click Save, then Restart when the page asks.
cameras: driveway: zones: driveway_apron: coordinates: 0,1,0.31,1,0.39,0.69,0,0.69 review: alerts: required_zones: - driveway_apronPer-object thresholds
Section titled “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(default0.5) — the floor for an individual detection. Any single box scoring below it is thrown away.threshold(default0.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:
- In the web UI, go to Settings → Global configuration → Objects. For one camera only, use Settings → Camera configuration → Objects and pick the camera.
- Make sure the class is switched on under Objects to track. If you just switched it on, click Save first: Object filters is built from the saved list of tracked objects.
- Under Object filters, open the object (for example
dog) and set Confidence threshold (threshold) to0.8and Minimum confidence (min_score) to0.6. Forperson, set Minimum object area (min_area) to1500. - Click Save.
objects: filters: person: min_area: 1500 dog: threshold: 0.8 min_score: 0.6Here 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
Section titled “Bringing your own model”Fregata supports any ONNX model that ONNX Runtime’s CoreML provider can run. The most common reasons to swap:
- You bought a Frigate+ subscription and want to use your custom-trained model. See below.
- You’ve trained YOLOv9 / YOLOv10 / RT-DETR yourself and have
an
onnxexport. Drop it in. - 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:
- In the web UI, go to Settings → System → Detectors and model.
- In the Detection Model card, open the Custom Model tab if the card has tabs (they appear once a Frigate+ API key is set); otherwise the model fields are shown directly.
- Replace Custom object detector model path (it shows the bundled
model’s path until you change it) with your file, for example
/Users/<you>/Fregata/config/models/my_model.onnx. - Set Object detection model input width and height to match the
model (for example
320and320), and Label map for custom object detector to your labels file (see below). - Under Advanced Settings, match Object Detection Model Type
(
yolo-generic,yolox,yolonas,dfine, orrfdetr), Model Input Tensor Shape (nchw), Model Input Pixel Color Format (rgb), and Model Input D Type (float) to how the model was exported. - Click Save, then Restart when the page asks.
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 width: 320 height: 320 input_tensor: nchw input_pixel_format: rgb input_dtype: float labelmap_path: /Users/<you>/Fregata/config/models/labels.txtA custom model needs its labelmap_path: a plain text file that
lists the model’s class labels in the order the model outputs them,
one per line. Fregata can’t know that order for a model it didn’t
ship, so without a labelmap the model’s classes are read through a
different layout and most labels come out wrong. (rfdetr models are
the exception: they emit COCO category ids and use the default
labelmap.)
If your model was trained on the standard 80 COCO classes (most YOLO exports are), use the labelmap that ships with Fregata:
On Settings → System → Detectors and model, in the model fields (the
Custom Model tab, if the card has tabs), set Label map for custom
object detector to /labelmap/coco-80.txt, then Save and
Restart.
model: labelmap_path: /labelmap/coco-80.txtThe bundled model and Frigate+ models don’t need this. The bundled
model already uses that file (so, as in Frigate’s own YOLO setups,
trucks and buses are reported as car), and a Frigate+ model carries
its labels with it.
Frigate+
Section titled “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 and picked a model, your config.yml points at it
via model.path:
- Add your Frigate+ API key as
PLUS_API_KEYin the tray’s Settings → Environment Variables… and restart Fregata (see Environment variables). Settings → Frigate+ shows whether the key was detected and validated. - Go to Settings → System → Detectors and model, and open the Frigate+ tab of the Detection Model card.
- Pick a model from Available Frigate+ models. Only models that list
onnxunder Supported Detectors are shown. - Click Save, then Restart when the page asks.
detectors: coreml: type: coreml
model: path: plus://abc123def456The PLUS_API_KEY environment variable still has to be set in the tray’s
Settings → Environment Variables….
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.
Verifying a model swap worked
Section titled “Verifying a model swap worked”After a config reload (or Restart Frigate from the tray):
- Watch the Detector row in the tray. The first inference logs
a warmup-tier classification —
ANE,GPU, orCPU. CPU after a model swap usually means an unsupported op. - Open the web UI’s System tab. The “Detector inference time” chart should plateau within a few seconds at the same tier.
- 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.