mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-02 09:15:22 +03:00
Merge branch 'release-0.10.0' of github.com:blakeblackshear/frigate into release-0.10.0
This commit is contained in:
commit
3aaa100fb3
@ -22,3 +22,5 @@ RUN pip3 install pylint black
|
|||||||
# Install Node 14
|
# Install Node 14
|
||||||
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - \
|
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - \
|
||||||
&& apt-get install -y nodejs
|
&& apt-get install -y nodejs
|
||||||
|
|
||||||
|
RUN npm install -g npm@latest
|
||||||
|
|||||||
@ -159,8 +159,9 @@ detect:
|
|||||||
enabled: True
|
enabled: True
|
||||||
# Optional: Number of frames without a detection before frigate considers an object to be gone. (default: 5x the frame rate)
|
# Optional: Number of frames without a detection before frigate considers an object to be gone. (default: 5x the frame rate)
|
||||||
max_disappeared: 25
|
max_disappeared: 25
|
||||||
# Optional: Frequency for running detection on stationary objects (default: 10x the frame rate)
|
# Optional: Frequency for running detection on stationary objects (default: 0)
|
||||||
stationary_interval: 50
|
# When set to 0, object detection will never be run on stationary objects. If set to 10, it will be run on every 10th frame.
|
||||||
|
stationary_interval: 0
|
||||||
|
|
||||||
# Optional: Object configuration
|
# Optional: Object configuration
|
||||||
# NOTE: Can be overridden at the camera level
|
# NOTE: Can be overridden at the camera level
|
||||||
@ -224,6 +225,9 @@ motion:
|
|||||||
record:
|
record:
|
||||||
# Optional: Enable recording (default: shown below)
|
# Optional: Enable recording (default: shown below)
|
||||||
enabled: False
|
enabled: False
|
||||||
|
# Optional: Number of minutes to wait between cleanup runs (default: shown below)
|
||||||
|
# This can be used to reduce the frequency of deleting recording segments from disk if you want to minimize i/o
|
||||||
|
expire_interval: 60
|
||||||
# Optional: Retention settings for recording
|
# Optional: Retention settings for recording
|
||||||
retain:
|
retain:
|
||||||
# Optional: Number of days to retain recordings regardless of events (default: shown below)
|
# Optional: Number of days to retain recordings regardless of events (default: shown below)
|
||||||
@ -264,7 +268,7 @@ record:
|
|||||||
# here, the segments will already be gone by the time this mode is applied.
|
# here, the segments will already be gone by the time this mode is applied.
|
||||||
# For example, if the camera retain mode is "motion", the segments without motion are
|
# For example, if the camera retain mode is "motion", the segments without motion are
|
||||||
# never stored, so setting the mode to "all" here won't bring them back.
|
# never stored, so setting the mode to "all" here won't bring them back.
|
||||||
mode: active_objects
|
mode: motion
|
||||||
# Optional: Per object retention days
|
# Optional: Per object retention days
|
||||||
objects:
|
objects:
|
||||||
person: 15
|
person: 15
|
||||||
|
|||||||
@ -62,6 +62,8 @@ cameras:
|
|||||||
roles:
|
roles:
|
||||||
- detect
|
- detect
|
||||||
- rtmp
|
- rtmp
|
||||||
|
rtmp:
|
||||||
|
enabled: False # <-- RTMP should be disabled if your stream is not H264
|
||||||
detect:
|
detect:
|
||||||
width: 1280 # <---- update for your camera's resolution
|
width: 1280 # <---- update for your camera's resolution
|
||||||
height: 720 # <---- update for your camera's resolution
|
height: 720 # <---- update for your camera's resolution
|
||||||
@ -71,7 +73,9 @@ cameras:
|
|||||||
|
|
||||||
At this point you should be able to start Frigate and see the the video feed in the UI.
|
At this point you should be able to start Frigate and see the the video feed in the UI.
|
||||||
|
|
||||||
If you get a green image from the camera, this means ffmpeg was not able to get the video feed from your camera. Check the logs for error messages from ffmpeg. The default ffmpeg arguments are designed to work with RTSP cameras that support TCP connections. FFmpeg arguments for other types of cameras can be found [here](/configuration/camera_specific).
|
If you get a green image from the camera, this means ffmpeg was not able to get the video feed from your camera. Check the logs for error messages from ffmpeg. The default ffmpeg arguments are designed to work with H264 RTSP cameras that support TCP connections. If you do not have H264 cameras, make sure you have disabled RTMP. It is possible to enable it, but you must tell ffmpeg to re-encode the video with customized output args.
|
||||||
|
|
||||||
|
FFmpeg arguments for other types of cameras can be found [here](/configuration/camera_specific).
|
||||||
|
|
||||||
### Step 5: Configure hardware acceleration (optional)
|
### Step 5: Configure hardware acceleration (optional)
|
||||||
|
|
||||||
|
|||||||
@ -55,7 +55,9 @@ Message published for each changed event. The first message is published when th
|
|||||||
"entered_zones": ["yard", "driveway"],
|
"entered_zones": ["yard", "driveway"],
|
||||||
"thumbnail": null,
|
"thumbnail": null,
|
||||||
"has_snapshot": false,
|
"has_snapshot": false,
|
||||||
"has_clip": false
|
"has_clip": false,
|
||||||
|
"motionless_count": 0, // number of frames the object has been motionless
|
||||||
|
"position_changes": 2 // number of times the object has changed position
|
||||||
},
|
},
|
||||||
"after": {
|
"after": {
|
||||||
"id": "1607123955.475377-mxklsc",
|
"id": "1607123955.475377-mxklsc",
|
||||||
@ -75,7 +77,9 @@ Message published for each changed event. The first message is published when th
|
|||||||
"entered_zones": ["yard", "driveway"],
|
"entered_zones": ["yard", "driveway"],
|
||||||
"thumbnail": null,
|
"thumbnail": null,
|
||||||
"has_snapshot": false,
|
"has_snapshot": false,
|
||||||
"has_clip": false
|
"has_clip": false,
|
||||||
|
"motionless_count": 0, // number of frames the object has been motionless
|
||||||
|
"position_changes": 2 // number of times the object has changed position
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
14859
docs/package-lock.json
generated
14859
docs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -12,13 +12,13 @@
|
|||||||
"clear": "docusaurus clear"
|
"clear": "docusaurus clear"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@docusaurus/core": "^2.0.0-beta.6",
|
"@docusaurus/core": "^2.0.0-beta.15",
|
||||||
"@docusaurus/preset-classic": "^2.0.0-beta.6",
|
"@docusaurus/preset-classic": "^2.0.0-beta.15",
|
||||||
"@mdx-js/react": "^1.6.21",
|
"@mdx-js/react": "^1.6.22",
|
||||||
"clsx": "^1.1.1",
|
"clsx": "^1.1.1",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
"react": "^16.8.4",
|
"react": "^16.14.0",
|
||||||
"react-dom": "^16.8.4"
|
"react-dom": "^16.14.0"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
@ -31,5 +31,8 @@
|
|||||||
"last 1 firefox version",
|
"last 1 firefox version",
|
||||||
"last 1 safari version"
|
"last 1 safari version"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^16.14.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,9 +72,7 @@ class RetainModeEnum(str, Enum):
|
|||||||
|
|
||||||
class RetainConfig(FrigateBaseModel):
|
class RetainConfig(FrigateBaseModel):
|
||||||
default: float = Field(default=10, title="Default retention period.")
|
default: float = Field(default=10, title="Default retention period.")
|
||||||
mode: RetainModeEnum = Field(
|
mode: RetainModeEnum = Field(default=RetainModeEnum.motion, title="Retain mode.")
|
||||||
default=RetainModeEnum.active_objects, title="Retain mode."
|
|
||||||
)
|
|
||||||
objects: Dict[str, float] = Field(
|
objects: Dict[str, float] = Field(
|
||||||
default_factory=dict, title="Object retention period."
|
default_factory=dict, title="Object retention period."
|
||||||
)
|
)
|
||||||
@ -103,6 +101,10 @@ class RecordRetainConfig(FrigateBaseModel):
|
|||||||
|
|
||||||
class RecordConfig(FrigateBaseModel):
|
class RecordConfig(FrigateBaseModel):
|
||||||
enabled: bool = Field(default=False, title="Enable record on all cameras.")
|
enabled: bool = Field(default=False, title="Enable record on all cameras.")
|
||||||
|
expire_interval: int = Field(
|
||||||
|
default=60,
|
||||||
|
title="Number of minutes to wait between cleanup runs.",
|
||||||
|
)
|
||||||
# deprecated - to be removed in a future version
|
# deprecated - to be removed in a future version
|
||||||
retain_days: Optional[float] = Field(title="Recording retention period in days.")
|
retain_days: Optional[float] = Field(title="Recording retention period in days.")
|
||||||
retain: RecordRetainConfig = Field(
|
retain: RecordRetainConfig = Field(
|
||||||
@ -171,8 +173,9 @@ class DetectConfig(FrigateBaseModel):
|
|||||||
title="Maximum number of frames the object can dissapear before detection ends."
|
title="Maximum number of frames the object can dissapear before detection ends."
|
||||||
)
|
)
|
||||||
stationary_interval: Optional[int] = Field(
|
stationary_interval: Optional[int] = Field(
|
||||||
|
default=0,
|
||||||
title="Frame interval for checking stationary objects.",
|
title="Frame interval for checking stationary objects.",
|
||||||
ge=1,
|
ge=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -473,7 +476,7 @@ class CameraLiveConfig(FrigateBaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class CameraConfig(FrigateBaseModel):
|
class CameraConfig(FrigateBaseModel):
|
||||||
name: Optional[str] = Field(title="Camera name.")
|
name: Optional[str] = Field(title="Camera name.", regex="^[a-zA-Z0-9_-]+$")
|
||||||
ffmpeg: CameraFfmpegConfig = Field(title="FFmpeg configuration for the camera.")
|
ffmpeg: CameraFfmpegConfig = Field(title="FFmpeg configuration for the camera.")
|
||||||
best_image_timeout: int = Field(
|
best_image_timeout: int = Field(
|
||||||
default=60,
|
default=60,
|
||||||
@ -763,11 +766,6 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
if camera_config.detect.max_disappeared is None:
|
if camera_config.detect.max_disappeared is None:
|
||||||
camera_config.detect.max_disappeared = max_disappeared
|
camera_config.detect.max_disappeared = max_disappeared
|
||||||
|
|
||||||
# Default stationary_interval configuration
|
|
||||||
stationary_interval = camera_config.detect.fps * 10
|
|
||||||
if camera_config.detect.stationary_interval is None:
|
|
||||||
camera_config.detect.stationary_interval = stationary_interval
|
|
||||||
|
|
||||||
# FFMPEG input substitution
|
# FFMPEG input substitution
|
||||||
for input in camera_config.ffmpeg.inputs:
|
for input in camera_config.ffmpeg.inputs:
|
||||||
input.path = input.path.format(**FRIGATE_ENV_VARS)
|
input.path = input.path.format(**FRIGATE_ENV_VARS)
|
||||||
|
|||||||
@ -167,6 +167,8 @@ def delete_event(id):
|
|||||||
if event.has_snapshot:
|
if event.has_snapshot:
|
||||||
media = Path(f"{os.path.join(CLIPS_DIR, media_name)}.jpg")
|
media = Path(f"{os.path.join(CLIPS_DIR, media_name)}.jpg")
|
||||||
media.unlink(missing_ok=True)
|
media.unlink(missing_ok=True)
|
||||||
|
media = Path(f"{os.path.join(CLIPS_DIR, media_name)}-clean.png")
|
||||||
|
media.unlink(missing_ok=True)
|
||||||
if event.has_clip:
|
if event.has_clip:
|
||||||
media = Path(f"{os.path.join(CLIPS_DIR, media_name)}.mp4")
|
media = Path(f"{os.path.join(CLIPS_DIR, media_name)}.mp4")
|
||||||
media.unlink(missing_ok=True)
|
media.unlink(missing_ok=True)
|
||||||
|
|||||||
@ -178,6 +178,7 @@ class TrackedObject:
|
|||||||
"area": self.obj_data["area"],
|
"area": self.obj_data["area"],
|
||||||
"region": self.obj_data["region"],
|
"region": self.obj_data["region"],
|
||||||
"motionless_count": self.obj_data["motionless_count"],
|
"motionless_count": self.obj_data["motionless_count"],
|
||||||
|
"position_changes": self.obj_data["position_changes"],
|
||||||
"current_zones": self.current_zones.copy(),
|
"current_zones": self.current_zones.copy(),
|
||||||
"entered_zones": self.entered_zones.copy(),
|
"entered_zones": self.entered_zones.copy(),
|
||||||
"has_clip": self.has_clip,
|
"has_clip": self.has_clip,
|
||||||
@ -266,7 +267,13 @@ class TrackedObject:
|
|||||||
box = self.thumbnail_data["box"]
|
box = self.thumbnail_data["box"]
|
||||||
box_size = 300
|
box_size = 300
|
||||||
region = calculate_region(
|
region = calculate_region(
|
||||||
best_frame.shape, box[0], box[1], box[2], box[3], box_size, multiplier=1.1
|
best_frame.shape,
|
||||||
|
box[0],
|
||||||
|
box[1],
|
||||||
|
box[2],
|
||||||
|
box[3],
|
||||||
|
box_size,
|
||||||
|
multiplier=1.1,
|
||||||
)
|
)
|
||||||
best_frame = best_frame[region[1] : region[3], region[0] : region[2]]
|
best_frame = best_frame[region[1] : region[3], region[0] : region[2]]
|
||||||
|
|
||||||
@ -732,6 +739,10 @@ class TrackedObjectProcessor(threading.Thread):
|
|||||||
if not snapshot_config.enabled:
|
if not snapshot_config.enabled:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# object never changed position
|
||||||
|
if obj.obj_data["position_changes"] == 0:
|
||||||
|
return False
|
||||||
|
|
||||||
# if there are required zones and there is no overlap
|
# if there are required zones and there is no overlap
|
||||||
required_zones = snapshot_config.required_zones
|
required_zones = snapshot_config.required_zones
|
||||||
if len(required_zones) > 0 and not set(obj.entered_zones) & set(required_zones):
|
if len(required_zones) > 0 and not set(obj.entered_zones) & set(required_zones):
|
||||||
@ -752,6 +763,10 @@ class TrackedObjectProcessor(threading.Thread):
|
|||||||
if not record_config.enabled:
|
if not record_config.enabled:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# object never changed position
|
||||||
|
if obj.obj_data["position_changes"] == 0:
|
||||||
|
return False
|
||||||
|
|
||||||
# If there are required zones and there is no overlap
|
# If there are required zones and there is no overlap
|
||||||
required_zones = record_config.events.required_zones
|
required_zones = record_config.events.required_zones
|
||||||
if len(required_zones) > 0 and not set(obj.entered_zones) & set(required_zones):
|
if len(required_zones) > 0 and not set(obj.entered_zones) & set(required_zones):
|
||||||
@ -773,6 +788,10 @@ class TrackedObjectProcessor(threading.Thread):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def should_mqtt_snapshot(self, camera, obj: TrackedObject):
|
def should_mqtt_snapshot(self, camera, obj: TrackedObject):
|
||||||
|
# object never changed position
|
||||||
|
if obj.obj_data["position_changes"] == 0:
|
||||||
|
return False
|
||||||
|
|
||||||
# if there are required zones and there is no overlap
|
# if there are required zones and there is no overlap
|
||||||
required_zones = self.config.cameras[camera].mqtt.required_zones
|
required_zones = self.config.cameras[camera].mqtt.required_zones
|
||||||
if len(required_zones) > 0 and not set(obj.entered_zones) & set(required_zones):
|
if len(required_zones) > 0 and not set(obj.entered_zones) & set(required_zones):
|
||||||
|
|||||||
@ -20,7 +20,9 @@ class ObjectTracker:
|
|||||||
def __init__(self, config: DetectConfig):
|
def __init__(self, config: DetectConfig):
|
||||||
self.tracked_objects = {}
|
self.tracked_objects = {}
|
||||||
self.disappeared = {}
|
self.disappeared = {}
|
||||||
|
self.positions = {}
|
||||||
self.max_disappeared = config.max_disappeared
|
self.max_disappeared = config.max_disappeared
|
||||||
|
self.detect_config = config
|
||||||
|
|
||||||
def register(self, index, obj):
|
def register(self, index, obj):
|
||||||
rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
||||||
@ -28,24 +30,83 @@ class ObjectTracker:
|
|||||||
obj["id"] = id
|
obj["id"] = id
|
||||||
obj["start_time"] = obj["frame_time"]
|
obj["start_time"] = obj["frame_time"]
|
||||||
obj["motionless_count"] = 0
|
obj["motionless_count"] = 0
|
||||||
|
obj["position_changes"] = 0
|
||||||
self.tracked_objects[id] = obj
|
self.tracked_objects[id] = obj
|
||||||
self.disappeared[id] = 0
|
self.disappeared[id] = 0
|
||||||
|
self.positions[id] = {
|
||||||
|
"xmins": [],
|
||||||
|
"ymins": [],
|
||||||
|
"xmaxs": [],
|
||||||
|
"ymaxs": [],
|
||||||
|
"xmin": 0,
|
||||||
|
"ymin": 0,
|
||||||
|
"xmax": self.detect_config.width,
|
||||||
|
"ymax": self.detect_config.height,
|
||||||
|
}
|
||||||
|
|
||||||
def deregister(self, id):
|
def deregister(self, id):
|
||||||
del self.tracked_objects[id]
|
del self.tracked_objects[id]
|
||||||
del self.disappeared[id]
|
del self.disappeared[id]
|
||||||
|
|
||||||
|
# tracks the current position of the object based on the last 10 bounding boxes
|
||||||
|
# returns False if the object has moved outside its previous position
|
||||||
|
def update_position(self, id, box):
|
||||||
|
position = self.positions[id]
|
||||||
|
position_box = (
|
||||||
|
position["xmin"],
|
||||||
|
position["ymin"],
|
||||||
|
position["xmax"],
|
||||||
|
position["ymax"],
|
||||||
|
)
|
||||||
|
|
||||||
|
xmin, ymin, xmax, ymax = box
|
||||||
|
|
||||||
|
iou = intersection_over_union(position_box, box)
|
||||||
|
|
||||||
|
# if the iou drops below the threshold
|
||||||
|
# assume the object has moved to a new position and reset the computed box
|
||||||
|
if iou < 0.6:
|
||||||
|
self.positions[id] = {
|
||||||
|
"xmins": [xmin],
|
||||||
|
"ymins": [ymin],
|
||||||
|
"xmaxs": [xmax],
|
||||||
|
"ymaxs": [ymax],
|
||||||
|
"xmin": xmin,
|
||||||
|
"ymin": ymin,
|
||||||
|
"xmax": xmax,
|
||||||
|
"ymax": ymax,
|
||||||
|
}
|
||||||
|
return False
|
||||||
|
|
||||||
|
# if there are less than 10 entries for the position, add the bounding box
|
||||||
|
# and recompute the position box
|
||||||
|
if len(position["xmins"]) < 10:
|
||||||
|
position["xmins"].append(xmin)
|
||||||
|
position["ymins"].append(ymin)
|
||||||
|
position["xmaxs"].append(xmax)
|
||||||
|
position["ymaxs"].append(ymax)
|
||||||
|
# by using percentiles here, we hopefully remove outliers
|
||||||
|
position["xmin"] = np.percentile(position["xmins"], 15)
|
||||||
|
position["ymin"] = np.percentile(position["ymins"], 15)
|
||||||
|
position["xmax"] = np.percentile(position["xmaxs"], 85)
|
||||||
|
position["ymax"] = np.percentile(position["ymaxs"], 85)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def update(self, id, new_obj):
|
def update(self, id, new_obj):
|
||||||
self.disappeared[id] = 0
|
self.disappeared[id] = 0
|
||||||
if (
|
# update the motionless count if the object has not moved to a new position
|
||||||
intersection_over_union(self.tracked_objects[id]["box"], new_obj["box"])
|
if self.update_position(id, new_obj["box"]):
|
||||||
> 0.9
|
|
||||||
):
|
|
||||||
self.tracked_objects[id]["motionless_count"] += 1
|
self.tracked_objects[id]["motionless_count"] += 1
|
||||||
else:
|
else:
|
||||||
self.tracked_objects[id]["motionless_count"] = 0
|
self.tracked_objects[id]["motionless_count"] = 0
|
||||||
|
self.tracked_objects[id]["position_changes"] += 1
|
||||||
self.tracked_objects[id].update(new_obj)
|
self.tracked_objects[id].update(new_obj)
|
||||||
|
|
||||||
|
def update_frame_times(self, frame_time):
|
||||||
|
for id in self.tracked_objects.keys():
|
||||||
|
self.tracked_objects[id]["frame_time"] = frame_time
|
||||||
|
|
||||||
def match_and_update(self, frame_time, new_objects):
|
def match_and_update(self, frame_time, new_objects):
|
||||||
# group by name
|
# group by name
|
||||||
new_object_groups = defaultdict(lambda: [])
|
new_object_groups = defaultdict(lambda: [])
|
||||||
|
|||||||
@ -497,7 +497,8 @@ class RecordingCleanup(threading.Thread):
|
|||||||
oldest_timestamp = datetime.datetime.now().timestamp()
|
oldest_timestamp = datetime.datetime.now().timestamp()
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
logger.warning(f"Unable to find file from recordings database: {p}")
|
logger.warning(f"Unable to find file from recordings database: {p}")
|
||||||
oldest_timestamp = datetime.datetime.now().timestamp()
|
Recordings.delete().where(Recordings.id == oldest_recording.id).execute()
|
||||||
|
return
|
||||||
|
|
||||||
logger.debug(f"Oldest recording in the db: {oldest_timestamp}")
|
logger.debug(f"Oldest recording in the db: {oldest_timestamp}")
|
||||||
process = sp.run(
|
process = sp.run(
|
||||||
@ -548,7 +549,7 @@ class RecordingCleanup(threading.Thread):
|
|||||||
# self.sync_recordings()
|
# self.sync_recordings()
|
||||||
|
|
||||||
# Expire tmp clips every minute, recordings and clean directories every hour.
|
# Expire tmp clips every minute, recordings and clean directories every hour.
|
||||||
for counter in itertools.cycle(range(60)):
|
for counter in itertools.cycle(range(self.config.record.expire_interval)):
|
||||||
if self.stop_event.wait(60):
|
if self.stop_event.wait(60):
|
||||||
logger.info(f"Exiting recording cleanup...")
|
logger.info(f"Exiting recording cleanup...")
|
||||||
break
|
break
|
||||||
|
|||||||
@ -1244,6 +1244,30 @@ class TestConfig(unittest.TestCase):
|
|||||||
runtime_config = frigate_config.runtime_config
|
runtime_config = frigate_config.runtime_config
|
||||||
assert runtime_config.cameras["back"].snapshots.retain.default == 1.5
|
assert runtime_config.cameras["back"].snapshots.retain.default == 1.5
|
||||||
|
|
||||||
|
def test_fails_on_bad_camera_name(self):
|
||||||
|
config = {
|
||||||
|
"mqtt": {"host": "mqtt"},
|
||||||
|
"snapshots": {"retain": {"default": 1.5}},
|
||||||
|
"cameras": {
|
||||||
|
"back camer#": {
|
||||||
|
"ffmpeg": {
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"path": "rtsp://10.0.0.1:554/video",
|
||||||
|
"roles": ["detect"],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
frigate_config = FrigateConfig(**config)
|
||||||
|
|
||||||
|
self.assertRaises(
|
||||||
|
ValidationError, lambda: frigate_config.runtime_config.cameras
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main(verbosity=2)
|
unittest.main(verbosity=2)
|
||||||
|
|||||||
111
frigate/video.py
111
frigate/video.py
@ -3,6 +3,7 @@ import itertools
|
|||||||
import logging
|
import logging
|
||||||
import multiprocessing as mp
|
import multiprocessing as mp
|
||||||
import queue
|
import queue
|
||||||
|
import random
|
||||||
import signal
|
import signal
|
||||||
import subprocess as sp
|
import subprocess as sp
|
||||||
import threading
|
import threading
|
||||||
@ -469,6 +470,8 @@ def process_frames(
|
|||||||
fps_tracker = EventsPerSecond()
|
fps_tracker = EventsPerSecond()
|
||||||
fps_tracker.start()
|
fps_tracker.start()
|
||||||
|
|
||||||
|
startup_scan_counter = 0
|
||||||
|
|
||||||
while not stop_event.is_set():
|
while not stop_event.is_set():
|
||||||
if exit_on_empty and frame_queue.empty():
|
if exit_on_empty and frame_queue.empty():
|
||||||
logger.info(f"Exiting track_objects...")
|
logger.info(f"Exiting track_objects...")
|
||||||
@ -512,7 +515,10 @@ def process_frames(
|
|||||||
# if there hasn't been motion for 10 frames
|
# if there hasn't been motion for 10 frames
|
||||||
if obj["motionless_count"] >= 10
|
if obj["motionless_count"] >= 10
|
||||||
# and it isn't due for a periodic check
|
# and it isn't due for a periodic check
|
||||||
and obj["motionless_count"] % detect_config.stationary_interval != 0
|
and (
|
||||||
|
detect_config.stationary_interval == 0
|
||||||
|
or obj["motionless_count"] % detect_config.stationary_interval != 0
|
||||||
|
)
|
||||||
# and it hasn't disappeared
|
# and it hasn't disappeared
|
||||||
and object_tracker.disappeared[obj["id"]] == 0
|
and object_tracker.disappeared[obj["id"]] == 0
|
||||||
# and it doesn't overlap with any current motion boxes
|
# and it doesn't overlap with any current motion boxes
|
||||||
@ -532,16 +538,39 @@ def process_frames(
|
|||||||
region_min_size = max(model_shape[0], model_shape[1])
|
region_min_size = max(model_shape[0], model_shape[1])
|
||||||
# compute regions
|
# compute regions
|
||||||
regions = [
|
regions = [
|
||||||
calculate_region(frame_shape, a[0], a[1], a[2], a[3], region_min_size, multiplier=1.2)
|
calculate_region(
|
||||||
|
frame_shape,
|
||||||
|
a[0],
|
||||||
|
a[1],
|
||||||
|
a[2],
|
||||||
|
a[3],
|
||||||
|
region_min_size,
|
||||||
|
multiplier=random.uniform(1.2, 1.5),
|
||||||
|
)
|
||||||
for a in combined_boxes
|
for a in combined_boxes
|
||||||
]
|
]
|
||||||
|
|
||||||
# consolidate regions with heavy overlap
|
# consolidate regions with heavy overlap
|
||||||
regions = [
|
regions = [
|
||||||
calculate_region(frame_shape, a[0], a[1], a[2], a[3], region_min_size, multiplier=1.0)
|
calculate_region(
|
||||||
|
frame_shape, a[0], a[1], a[2], a[3], region_min_size, multiplier=1.0
|
||||||
|
)
|
||||||
for a in reduce_boxes(regions, 0.4)
|
for a in reduce_boxes(regions, 0.4)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# if starting up, get the next startup scan region
|
||||||
|
if startup_scan_counter < 9:
|
||||||
|
ymin = int(frame_shape[0] / 3 * startup_scan_counter / 3)
|
||||||
|
ymax = int(frame_shape[0] / 3 + ymin)
|
||||||
|
xmin = int(frame_shape[1] / 3 * startup_scan_counter / 3)
|
||||||
|
xmax = int(frame_shape[1] / 3 + xmin)
|
||||||
|
regions.append(
|
||||||
|
calculate_region(
|
||||||
|
frame_shape, xmin, ymin, xmax, ymax, region_min_size, multiplier=1.2
|
||||||
|
)
|
||||||
|
)
|
||||||
|
startup_scan_counter += 1
|
||||||
|
|
||||||
# resize regions and detect
|
# resize regions and detect
|
||||||
# seed with stationary objects
|
# seed with stationary objects
|
||||||
detections = [
|
detections = [
|
||||||
@ -555,6 +584,7 @@ def process_frames(
|
|||||||
for obj in object_tracker.tracked_objects.values()
|
for obj in object_tracker.tracked_objects.values()
|
||||||
if obj["id"] in stationary_object_ids
|
if obj["id"] in stationary_object_ids
|
||||||
]
|
]
|
||||||
|
|
||||||
for region in regions:
|
for region in regions:
|
||||||
detections.extend(
|
detections.extend(
|
||||||
detect(
|
detect(
|
||||||
@ -570,7 +600,7 @@ def process_frames(
|
|||||||
#########
|
#########
|
||||||
# merge objects, check for clipped objects and look again up to 4 times
|
# merge objects, check for clipped objects and look again up to 4 times
|
||||||
#########
|
#########
|
||||||
refining = True
|
refining = len(regions) > 0
|
||||||
refine_count = 0
|
refine_count = 0
|
||||||
while refining and refine_count < 4:
|
while refining and refine_count < 4:
|
||||||
refining = False
|
refining = False
|
||||||
@ -625,44 +655,49 @@ def process_frames(
|
|||||||
|
|
||||||
## drop detections that overlap too much
|
## drop detections that overlap too much
|
||||||
consolidated_detections = []
|
consolidated_detections = []
|
||||||
# group by name
|
|
||||||
detected_object_groups = defaultdict(lambda: [])
|
|
||||||
for detection in detections:
|
|
||||||
detected_object_groups[detection[0]].append(detection)
|
|
||||||
|
|
||||||
# loop over detections grouped by label
|
# if detection was run on this frame, consolidate
|
||||||
for group in detected_object_groups.values():
|
if len(regions) > 0:
|
||||||
# if the group only has 1 item, skip
|
# group by name
|
||||||
if len(group) == 1:
|
detected_object_groups = defaultdict(lambda: [])
|
||||||
consolidated_detections.append(group[0])
|
for detection in detections:
|
||||||
continue
|
detected_object_groups[detection[0]].append(detection)
|
||||||
|
|
||||||
# sort smallest to largest by area
|
# loop over detections grouped by label
|
||||||
sorted_by_area = sorted(group, key=lambda g: g[3])
|
for group in detected_object_groups.values():
|
||||||
|
# if the group only has 1 item, skip
|
||||||
|
if len(group) == 1:
|
||||||
|
consolidated_detections.append(group[0])
|
||||||
|
continue
|
||||||
|
|
||||||
for current_detection_idx in range(0, len(sorted_by_area)):
|
# sort smallest to largest by area
|
||||||
current_detection = sorted_by_area[current_detection_idx][2]
|
sorted_by_area = sorted(group, key=lambda g: g[3])
|
||||||
overlap = 0
|
|
||||||
for to_check_idx in range(
|
for current_detection_idx in range(0, len(sorted_by_area)):
|
||||||
min(current_detection_idx + 1, len(sorted_by_area)),
|
current_detection = sorted_by_area[current_detection_idx][2]
|
||||||
len(sorted_by_area),
|
overlap = 0
|
||||||
):
|
for to_check_idx in range(
|
||||||
to_check = sorted_by_area[to_check_idx][2]
|
min(current_detection_idx + 1, len(sorted_by_area)),
|
||||||
# if 90% of smaller detection is inside of another detection, consolidate
|
len(sorted_by_area),
|
||||||
if (
|
|
||||||
area(intersection(current_detection, to_check))
|
|
||||||
/ area(current_detection)
|
|
||||||
> 0.9
|
|
||||||
):
|
):
|
||||||
overlap = 1
|
to_check = sorted_by_area[to_check_idx][2]
|
||||||
break
|
# if 90% of smaller detection is inside of another detection, consolidate
|
||||||
if overlap == 0:
|
if (
|
||||||
consolidated_detections.append(
|
area(intersection(current_detection, to_check))
|
||||||
sorted_by_area[current_detection_idx]
|
/ area(current_detection)
|
||||||
)
|
> 0.9
|
||||||
|
):
|
||||||
# now that we have refined our detections, we need to track objects
|
overlap = 1
|
||||||
object_tracker.match_and_update(frame_time, consolidated_detections)
|
break
|
||||||
|
if overlap == 0:
|
||||||
|
consolidated_detections.append(
|
||||||
|
sorted_by_area[current_detection_idx]
|
||||||
|
)
|
||||||
|
# now that we have refined our detections, we need to track objects
|
||||||
|
object_tracker.match_and_update(frame_time, consolidated_detections)
|
||||||
|
# else, just update the frame times for the stationary objects
|
||||||
|
else:
|
||||||
|
object_tracker.update_frame_times(frame_time)
|
||||||
|
|
||||||
# add to the queue if not full
|
# add to the queue if not full
|
||||||
if detected_objects_queue.full():
|
if detected_objects_queue.full():
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user