From 34cc1208a65c8e63833ecf41e20da798fbe3cafc Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 5 Mar 2026 18:20:03 -0600 Subject: [PATCH] Skip motion threshold configuration (#22255) * backend * frontend * i18n * docs * add test * clean up * clean up motion detection docs * formatting * make optional --- docs/docs/configuration/motion_detection.md | 46 +++++++--- docs/docs/configuration/reference.md | 12 ++- frigate/config/camera/motion.py | 9 +- frigate/motion/improved_motion.py | 23 ++++- frigate/test/test_motion_detector.py | 91 +++++++++++++++++++ web/public/locales/en/config/cameras.json | 9 +- web/public/locales/en/config/global.json | 6 +- .../config-form/section-configs/motion.ts | 3 + web/src/types/frigateConfig.ts | 1 + 9 files changed, 178 insertions(+), 22 deletions(-) create mode 100644 frigate/test/test_motion_detector.py diff --git a/docs/docs/configuration/motion_detection.md b/docs/docs/configuration/motion_detection.md index c22491fd0..53e63272a 100644 --- a/docs/docs/configuration/motion_detection.md +++ b/docs/docs/configuration/motion_detection.md @@ -38,7 +38,6 @@ Remember that motion detection is just used to determine when object detection s The threshold value dictates how much of a change in a pixels luminance is required to be considered motion. ```yaml -# default threshold value motion: # Optional: The threshold passed to cv2.threshold to determine if a pixel is different enough to be counted as motion. (default: shown below) # Increasing this value will make motion detection less sensitive and decreasing it will make motion detection more sensitive. @@ -53,7 +52,6 @@ Watching the motion boxes in the debug view, increase the threshold until you on ### Contour Area ```yaml -# default contour_area value motion: # Optional: Minimum size in pixels in the resized motion image that counts as motion (default: shown below) # Increasing this value will prevent smaller areas of motion from being detected. Decreasing will @@ -81,27 +79,49 @@ However, if the preferred day settings do not work well at night it is recommend ## Tuning For Large Changes In Motion +### Lightning Threshold + ```yaml -# default lightning_threshold: motion: - # Optional: The percentage of the image used to detect lightning or other substantial changes where motion detection - # needs to recalibrate. (default: shown below) - # Increasing this value will make motion detection more likely to consider lightning or ir mode changes as valid motion. - # Decreasing this value will make motion detection more likely to ignore large amounts of motion such as a person approaching - # a doorbell camera. + # Optional: The percentage of the image used to detect lightning or + # other substantial changes where motion detection needs to + # recalibrate. (default: shown below) + # Increasing this value will make motion detection more likely + # to consider lightning or IR mode changes as valid motion. + # Decreasing this value will make motion detection more likely + # to ignore large amounts of motion such as a person + # approaching a doorbell camera. lightning_threshold: 0.8 ``` +Large changes in motion like PTZ moves and camera switches between Color and IR mode should result in a pause in object detection. `lightning_threshold` defines the percentage of the image used to detect these substantial changes. Increasing this value makes motion detection more likely to treat large changes (like IR mode switches) as valid motion. Decreasing it makes motion detection more likely to ignore large amounts of motion, such as a person approaching a doorbell camera. + +Note that `lightning_threshold` does **not** stop motion-based recordings from being saved — it only prevents additional motion analysis after the threshold is exceeded, reducing false positive object detections during high-motion periods (e.g. storms or PTZ sweeps) without interfering with recordings. + :::warning -Some cameras like doorbell cameras may have missed detections when someone walks directly in front of the camera and the lightning_threshold causes motion detection to be re-calibrated. In this case, it may be desirable to increase the `lightning_threshold` to ensure these objects are not missed. +Some cameras, like doorbell cameras, may have missed detections when someone walks directly in front of the camera and the `lightning_threshold` causes motion detection to recalibrate. In this case, it may be desirable to increase the `lightning_threshold` to ensure these objects are not missed. ::: -:::note +### Skip Motion On Large Scene Changes -Lightning threshold does not stop motion based recordings from being saved. +```yaml +motion: + # Optional: Fraction of the frame that must change in a single update + # before Frigate will completely ignore any motion in that frame. + # Values range between 0.0 and 1.0, leave unset (null) to disable. + # Setting this to 0.7 would cause Frigate to **skip** reporting + # motion boxes when more than 70% of the image appears to change + # (e.g. during lightning storms, IR/color mode switches, or other + # sudden lighting events). + skip_motion_threshold: 0.7 +``` + +This option is handy when you want to prevent large transient changes from triggering recordings or object detection. It differs from `lightning_threshold` because it completely suppresses motion instead of just forcing a recalibration. + +:::warning + +When the skip threshold is exceeded, **no motion is reported** for that frame, meaning **nothing is recorded** for that frame. That means you can miss something important, like a PTZ camera auto-tracking an object or activity while the camera is moving. If you prefer to guarantee that every frame is saved, leave this unset and accept occasional recordings containing scene noise — they typically only take up a few megabytes and are quick to scan in the timeline UI. ::: - -Large changes in motion like PTZ moves and camera switches between Color and IR mode should result in a pause in object detection. This is done via the `lightning_threshold` configuration. It is defined as the percentage of the image used to detect lightning or other substantial changes where motion detection needs to recalibrate. Increasing this value will make motion detection more likely to consider lightning or IR mode changes as valid motion. Decreasing this value will make motion detection more likely to ignore large amounts of motion such as a person approaching a doorbell camera. diff --git a/docs/docs/configuration/reference.md b/docs/docs/configuration/reference.md index 3efc4f0ed..cac508195 100644 --- a/docs/docs/configuration/reference.md +++ b/docs/docs/configuration/reference.md @@ -480,12 +480,16 @@ motion: # Increasing this value will make motion detection less sensitive and decreasing it will make motion detection more sensitive. # The value should be between 1 and 255. threshold: 30 - # Optional: The percentage of the image used to detect lightning or other substantial changes where motion detection - # needs to recalibrate. (default: shown below) + # Optional: The percentage of the image used to detect lightning or other substantial changes where motion detection needs + # to recalibrate and motion checks stop for that frame. Recordings are unaffected. (default: shown below) # Increasing this value will make motion detection more likely to consider lightning or ir mode changes as valid motion. - # Decreasing this value will make motion detection more likely to ignore large amounts of motion such as a person approaching - # a doorbell camera. + # Decreasing this value will make motion detection more likely to ignore large amounts of motion such as a person approaching a doorbell camera. lightning_threshold: 0.8 + # Optional: Fraction of the frame that must change in a single update before motion boxes are completely + # ignored. Values range between 0.0 and 1.0. When exceeded, no motion boxes are reported and **no motion + # recording** is created for that frame. Leave unset (null) to disable this feature. Use with care on PTZ + # cameras or other situations where you require guaranteed frame capture. + skip_motion_threshold: None # Optional: Minimum size in pixels in the resized motion image that counts as motion (default: shown below) # Increasing this value will prevent smaller areas of motion from being detected. Decreasing will # make motion detection more sensitive to smaller moving objects. diff --git a/frigate/config/camera/motion.py b/frigate/config/camera/motion.py index b6877693b..ebba8613c 100644 --- a/frigate/config/camera/motion.py +++ b/frigate/config/camera/motion.py @@ -24,10 +24,17 @@ class MotionConfig(FrigateBaseModel): lightning_threshold: float = Field( default=0.8, title="Lightning threshold", - description="Threshold to detect and ignore brief lighting spikes (lower is more sensitive, values between 0.3 and 1.0).", + description="Threshold to detect and ignore brief lighting spikes (lower is more sensitive, values between 0.3 and 1.0). This does not prevent motion detection entirely; it merely causes the detector to stop analyzing additional frames once the threshold is exceeded. Motion-based recordings are still created during these events.", ge=0.3, le=1.0, ) + skip_motion_threshold: Optional[float] = Field( + default=None, + title="Skip motion threshold", + description="If set to a value between 0.0 and 1.0, and more than this fraction of the image changes in a single frame, the detector will return no motion boxes and immediately recalibrate. This can save CPU and reduce false positives during lightning, storms, etc., but may miss real events such as a PTZ camera auto‑tracking an object. The trade‑off is between dropping a few megabytes of recordings versus reviewing a couple short clips. Leave unset (None) to disable this feature.", + ge=0.0, + le=1.0, + ) improve_contrast: bool = Field( default=True, title="Improve contrast", diff --git a/frigate/motion/improved_motion.py b/frigate/motion/improved_motion.py index a8f2ab12c..b821e9532 100644 --- a/frigate/motion/improved_motion.py +++ b/frigate/motion/improved_motion.py @@ -176,11 +176,32 @@ class ImprovedMotionDetector(MotionDetector): motion_boxes = [] pct_motion = 0 + # skip motion entirely if the scene change percentage exceeds configured + # threshold. this is useful to ignore lighting storms, IR mode switches, + # etc. rather than registering them as brief motion and then recalibrating. + # note: skipping means the frame is dropped and **no recording will be + # created**, which could hide a legitimate object if the camera is actively + # auto‑tracking. the alternative is to allow motion and accept a small + # recording that can be reviewed in the timeline. disabled by default (None). + if ( + self.config.skip_motion_threshold is not None + and pct_motion > self.config.skip_motion_threshold + ): + # force a recalibration so we transition to the new background + self.calibrating = True + return [] + # once the motion is less than 5% and the number of contours is < 4, assume its calibrated if pct_motion < 0.05 and len(motion_boxes) <= 4: self.calibrating = False - # if calibrating or the motion contours are > 80% of the image area (lightning, ir, ptz) recalibrate + # if calibrating or the motion contours are > 80% of the image area + # (lightning, ir, ptz) recalibrate. the lightning threshold does **not** + # stop motion detection entirely; it simply halts additional processing for + # the current frame once the percentage crosses the threshold. this helps + # reduce false positive object detections and CPU usage during high‑motion + # events. recordings continue to be generated because users expect data + # while a PTZ camera is moving. if self.calibrating or pct_motion > self.config.lightning_threshold: self.calibrating = True diff --git a/frigate/test/test_motion_detector.py b/frigate/test/test_motion_detector.py new file mode 100644 index 000000000..cdf4210a5 --- /dev/null +++ b/frigate/test/test_motion_detector.py @@ -0,0 +1,91 @@ +import unittest + +import numpy as np + +from frigate.config.camera.motion import MotionConfig +from frigate.motion.improved_motion import ImprovedMotionDetector + + +class TestImprovedMotionDetector(unittest.TestCase): + def setUp(self): + # small frame for testing; actual frames are grayscale + self.frame_shape = (100, 100) # height, width + self.config = MotionConfig() + # motion detector assumes a rasterized_mask attribute exists on config + # when update_mask() is called; add one manually by bypassing pydantic. + object.__setattr__( + self.config, + "rasterized_mask", + np.ones((self.frame_shape[0], self.frame_shape[1]), dtype=np.uint8), + ) + + # create minimal PTZ metrics stub to satisfy detector checks + class _Stub: + def __init__(self, value=False): + self.value = value + + def is_set(self): + return bool(self.value) + + class DummyPTZ: + def __init__(self): + self.autotracker_enabled = _Stub(False) + self.motor_stopped = _Stub(False) + self.stop_time = _Stub(0) + + self.detector = ImprovedMotionDetector( + self.frame_shape, self.config, fps=30, ptz_metrics=DummyPTZ() + ) + + # establish a baseline frame (all zeros) + base_frame = np.zeros( + (self.frame_shape[0], self.frame_shape[1]), dtype=np.uint8 + ) + self.detector.detect(base_frame) + + def _half_change_frame(self) -> np.ndarray: + """Produce a frame where roughly half of the pixels are different.""" + frame = np.zeros((self.frame_shape[0], self.frame_shape[1]), dtype=np.uint8) + # flip the top half to white + frame[: self.frame_shape[0] // 2, :] = 255 + return frame + + def test_skip_motion_threshold_default(self): + """With the default (None) setting, motion should always be reported.""" + frame = self._half_change_frame() + boxes = self.detector.detect(frame) + self.assertTrue( + boxes, "Expected motion boxes when skip threshold is unset (disabled)" + ) + + def test_skip_motion_threshold_applied(self): + """Setting a low skip threshold should prevent any boxes from being returned.""" + # change the config and update the detector reference + self.config.skip_motion_threshold = 0.4 + self.detector.config = self.config + self.detector.update_mask() + + frame = self._half_change_frame() + boxes = self.detector.detect(frame) + self.assertEqual( + boxes, + [], + "Motion boxes should be empty when scene change exceeds skip threshold", + ) + + def test_skip_motion_threshold_does_not_affect_calibration(self): + """Even when skipping, the detector should go into calibrating state.""" + self.config.skip_motion_threshold = 0.4 + self.detector.config = self.config + self.detector.update_mask() + + frame = self._half_change_frame() + _ = self.detector.detect(frame) + self.assertTrue( + self.detector.calibrating, + "Detector should be in calibrating state after skip event", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/web/public/locales/en/config/cameras.json b/web/public/locales/en/config/cameras.json index bbb8d4b45..5880d30c3 100644 --- a/web/public/locales/en/config/cameras.json +++ b/web/public/locales/en/config/cameras.json @@ -264,7 +264,11 @@ }, "lightning_threshold": { "label": "Lightning threshold", - "description": "Threshold to detect and ignore brief lighting spikes (lower is more sensitive, values between 0.3 and 1.0)." + "description": "Threshold to detect and ignore brief lighting spikes (lower is more sensitive, values between 0.3 and 1.0). This does not prevent motion detection entirely; it merely causes the detector to stop analyzing additional frames once the threshold is exceeded. Motion-based recordings are still created during these events." + }, + "skip_motion_threshold": { + "label": "Skip motion threshold", + "description": "If more than this fraction of the image changes in a single frame, the detector will return no motion boxes and immediately recalibrate. This can save CPU and reduce false positives during lightning, storms, etc., but may miss real events such as a PTZ camera auto‑tracking an object. The trade‑off is between dropping a few megabytes of recordings versus reviewing a couple short clips. Range 0.0 to 1.0." }, "improve_contrast": { "label": "Improve contrast", @@ -864,7 +868,8 @@ "description": "A user-friendly name for the zone, displayed in the Frigate UI. If not set, a formatted version of the zone name will be used." }, "enabled": { - "label": "Whether this zone is active. Disabled zones are ignored at runtime." + "label": "Enabled", + "description": "Enable or disable this zone. Disabled zones are ignored at runtime." }, "enabled_in_config": { "label": "Keep track of original state of zone." diff --git a/web/public/locales/en/config/global.json b/web/public/locales/en/config/global.json index 1bda2ab44..5268c1b02 100644 --- a/web/public/locales/en/config/global.json +++ b/web/public/locales/en/config/global.json @@ -1391,7 +1391,11 @@ }, "lightning_threshold": { "label": "Lightning threshold", - "description": "Threshold to detect and ignore brief lighting spikes (lower is more sensitive, values between 0.3 and 1.0)." + "description": "Threshold to detect and ignore brief lighting spikes (lower is more sensitive, values between 0.3 and 1.0). This does not prevent motion detection entirely; it merely causes the detector to stop analyzing additional frames once the threshold is exceeded. Motion-based recordings are still created during these events." + }, + "skip_motion_threshold": { + "label": "Skip motion threshold", + "description": "If more than this fraction of the image changes in a single frame, the detector will return no motion boxes and immediately recalibrate. This can save CPU and reduce false positives during lightning, storms, etc., but may miss real events such as a PTZ camera auto‑tracking an object. The trade‑off is between dropping a few megabytes of recordings versus reviewing a couple short clips. Range 0.0 to 1.0." }, "improve_contrast": { "label": "Improve contrast", diff --git a/web/src/components/config-form/section-configs/motion.ts b/web/src/components/config-form/section-configs/motion.ts index 41b5738d3..edc0b6e14 100644 --- a/web/src/components/config-form/section-configs/motion.ts +++ b/web/src/components/config-form/section-configs/motion.ts @@ -8,6 +8,7 @@ const motion: SectionConfigOverrides = { "enabled", "threshold", "lightning_threshold", + "skip_motion_threshold", "improve_contrast", "contour_area", "delta_alpha", @@ -22,6 +23,7 @@ const motion: SectionConfigOverrides = { hiddenFields: ["enabled_in_config", "mask", "raw_mask"], advancedFields: [ "lightning_threshold", + "skip_motion_threshold", "delta_alpha", "frame_alpha", "frame_height", @@ -33,6 +35,7 @@ const motion: SectionConfigOverrides = { "enabled", "threshold", "lightning_threshold", + "skip_motion_threshold", "improve_contrast", "contour_area", "delta_alpha", diff --git a/web/src/types/frigateConfig.ts b/web/src/types/frigateConfig.ts index dc3554940..dcf3c312f 100644 --- a/web/src/types/frigateConfig.ts +++ b/web/src/types/frigateConfig.ts @@ -106,6 +106,7 @@ export interface CameraConfig { frame_height: number; improve_contrast: boolean; lightning_threshold: number; + skip_motion_threshold: number | null; mask: { [maskId: string]: { friendly_name?: string;