diff --git a/docs/docs/configuration/reference.md b/docs/docs/configuration/reference.md index 2ba42224d..b22c115c9 100644 --- a/docs/docs/configuration/reference.md +++ b/docs/docs/configuration/reference.md @@ -488,6 +488,8 @@ cameras: coordinates: 545,1077,747,939,788,805 # Optional: Number of consecutive frames required for object to be considered present in the zone (default: shown below). inertia: 3 + # Optional: Number of seconds that an object must loiter to be considered in the zone (default: shown below) + loitering_time: 0 # Optional: List of objects that can trigger this zone (default: all tracked objects) objects: - person diff --git a/docs/docs/configuration/zones.md b/docs/docs/configuration/zones.md index 40297b048..964b44f17 100644 --- a/docs/docs/configuration/zones.md +++ b/docs/docs/configuration/zones.md @@ -60,6 +60,19 @@ camera: Only car objects can trigger the `front_yard_street` zone and only person can trigger the `entire_yard`. You will get events for person objects that enter anywhere in the yard, and events for cars only if they enter the street. +### Zone Loitering + +Sometimes objects are expected to be passing through a zone, but an object loitering in an area is unexpected. Zones can be configured to have a minimum loitering time before the object will be considered in the zone. + +```yaml +camera: + zones: + sidewalk: + loitering_time: 4 # unit is in seconds + objects: + - person +``` + ### Zone Inertia Sometimes an objects bounding box may be slightly incorrect and the bottom center of the bounding box is inside the zone while the object is not actually in the zone. Zone inertia helps guard against this by requiring an object's bounding box to be within the zone for multiple consecutive frames. This value can be configured: diff --git a/frigate/config.py b/frigate/config.py index 85377b2f9..55acc1a44 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -515,6 +515,11 @@ class ZoneConfig(BaseModel): title="Number of consecutive frames required for object to be considered present in the zone.", gt=0, ) + loitering_time: int = Field( + default=0, + ge=0, + title="Number of seconds that an object must loiter to be considered in the zone.", + ) objects: List[str] = Field( default_factory=list, title="List of objects that can trigger the zone.", diff --git a/frigate/object_processing.py b/frigate/object_processing.py index f3bb54adc..4cbaa11d6 100644 --- a/frigate/object_processing.py +++ b/frigate/object_processing.py @@ -116,7 +116,8 @@ class TrackedObject: self.colormap = colormap self.camera_config = camera_config self.frame_cache = frame_cache - self.zone_presence = {} + self.zone_presence: dict[str, int] = {} + self.zone_loitering: dict[str, int] = {} self.current_zones = [] self.entered_zones = [] self.attributes = defaultdict(float) @@ -189,19 +190,28 @@ class TrackedObject: if len(zone.objects) > 0 and obj_data["label"] not in zone.objects: continue contour = zone.contour - zone_score = self.zone_presence.get(name, 0) + zone_score = self.zone_presence.get(name, 0) + 1 # check if the object is in the zone if cv2.pointPolygonTest(contour, bottom_center, False) >= 0: # if the object passed the filters once, dont apply again if name in self.current_zones or not zone_filtered(self, zone.filters): - self.zone_presence[name] = zone_score + 1 - # an object is only considered present in a zone if it has a zone inertia of 3+ - if self.zone_presence[name] >= zone.inertia: - current_zones.append(name) + if zone_score >= zone.inertia: + loitering_score = self.zone_loitering.get(name, 0) + 1 - if name not in self.entered_zones: - self.entered_zones.append(name) + # loitering time is configured as seconds, convert to count of frames + if loitering_score >= ( + self.camera_config.zones[name].loitering_time + * self.camera_config.detect.fps + ): + current_zones.append(name) + + if name not in self.entered_zones: + self.entered_zones.append(name) + else: + self.zone_loitering[name] = loitering_score + else: + self.zone_presence[name] = zone_score else: # once an object has a zone inertia of 3+ it is not checked anymore if 0 < zone_score < zone.inertia: