forked from Github/frigate
Motion timeline data (#10245)
* Refactor activity api to send motion and audio data * Prepare for using motion data timeline * Get working * reduce to 0 * fix * Formatting * fix typing * add motion data to timelines and allow motion cameas to be selectable * Fix tests * cleanup * Fix not loading preview when changing hours
This commit is contained in:
@@ -5,12 +5,9 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import traceback
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
from functools import reduce
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import requests
|
||||
from flask import (
|
||||
Blueprint,
|
||||
@@ -31,7 +28,7 @@ from frigate.api.review import ReviewBp
|
||||
from frigate.config import FrigateConfig
|
||||
from frigate.const import CONFIG_DIR
|
||||
from frigate.events.external import ExternalEventProcessor
|
||||
from frigate.models import Event, Recordings, Timeline
|
||||
from frigate.models import Event, Timeline
|
||||
from frigate.plus import PlusApi
|
||||
from frigate.ptz.onvif import OnvifController
|
||||
from frigate.stats.emitter import StatsEmitter
|
||||
@@ -632,102 +629,3 @@ def hourly_timeline():
|
||||
"hours": hours,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/<camera_name>/recording/hourly/activity")
|
||||
def hourly_timeline_activity(camera_name: str):
|
||||
"""Get hourly summary for timeline."""
|
||||
if camera_name not in current_app.frigate_config.cameras:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Camera not found"}),
|
||||
404,
|
||||
)
|
||||
|
||||
before = request.args.get("before", type=float, default=datetime.now())
|
||||
after = request.args.get(
|
||||
"after", type=float, default=datetime.now() - timedelta(hours=1)
|
||||
)
|
||||
tz_name = request.args.get("timezone", default="utc", type=str)
|
||||
|
||||
_, minute_modifier, _ = get_tz_modifiers(tz_name)
|
||||
minute_offset = int(minute_modifier.split(" ")[0])
|
||||
|
||||
all_recordings: list[Recordings] = (
|
||||
Recordings.select(
|
||||
Recordings.start_time,
|
||||
Recordings.duration,
|
||||
Recordings.objects,
|
||||
Recordings.motion,
|
||||
)
|
||||
.where(Recordings.camera == camera_name)
|
||||
.where(Recordings.motion > 0)
|
||||
.where((Recordings.start_time > after) & (Recordings.end_time < before))
|
||||
.order_by(Recordings.start_time.asc())
|
||||
.iterator()
|
||||
)
|
||||
|
||||
# data format is ex:
|
||||
# {timestamp: [{ date: 1, count: 1, type: motion }]}] }}
|
||||
hours: dict[int, list[dict[str, any]]] = defaultdict(list)
|
||||
|
||||
key = datetime.fromtimestamp(after).replace(second=0, microsecond=0) + timedelta(
|
||||
minutes=minute_offset
|
||||
)
|
||||
check = (key + timedelta(hours=1)).timestamp()
|
||||
|
||||
# set initial start so data is representative of full hour
|
||||
hours[int(key.timestamp())].append(
|
||||
[
|
||||
key.timestamp(),
|
||||
0,
|
||||
False,
|
||||
]
|
||||
)
|
||||
|
||||
for recording in all_recordings:
|
||||
if recording.start_time > check:
|
||||
hours[int(key.timestamp())].append(
|
||||
[
|
||||
(key + timedelta(minutes=59, seconds=59)).timestamp(),
|
||||
0,
|
||||
False,
|
||||
]
|
||||
)
|
||||
key = key + timedelta(hours=1)
|
||||
check = (key + timedelta(hours=1)).timestamp()
|
||||
hours[int(key.timestamp())].append(
|
||||
[
|
||||
key.timestamp(),
|
||||
0,
|
||||
False,
|
||||
]
|
||||
)
|
||||
|
||||
data_type = recording.objects > 0
|
||||
count = recording.motion + recording.objects
|
||||
hours[int(key.timestamp())].append(
|
||||
[
|
||||
recording.start_time + (recording.duration / 2),
|
||||
0 if count == 0 else np.log2(count),
|
||||
data_type,
|
||||
]
|
||||
)
|
||||
|
||||
# resample data using pandas to get activity on minute to minute basis
|
||||
for key, data in hours.items():
|
||||
df = pd.DataFrame(data, columns=["date", "count", "hasObjects"])
|
||||
|
||||
# set date as datetime index
|
||||
df["date"] = pd.to_datetime(df["date"], unit="s")
|
||||
df.set_index(["date"], inplace=True)
|
||||
|
||||
# normalize data
|
||||
df = df.resample("T").mean().fillna(0)
|
||||
|
||||
# change types for output
|
||||
df.index = df.index.astype(int) // (10**9)
|
||||
df["count"] = df["count"].astype(int)
|
||||
df["hasObjects"] = df["hasObjects"].astype(bool)
|
||||
hours[key] = df.reset_index().to_dict("records")
|
||||
|
||||
return jsonify(hours)
|
||||
|
||||
@@ -4,6 +4,7 @@ import logging
|
||||
from datetime import datetime, timedelta
|
||||
from functools import reduce
|
||||
|
||||
import pandas as pd
|
||||
from flask import (
|
||||
Blueprint,
|
||||
jsonify,
|
||||
@@ -12,7 +13,7 @@ from flask import (
|
||||
)
|
||||
from peewee import Case, DoesNotExist, fn, operator
|
||||
|
||||
from frigate.models import ReviewSegment
|
||||
from frigate.models import Recordings, ReviewSegment
|
||||
from frigate.util.builtin import get_tz_modifiers
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -258,3 +259,66 @@ def delete_reviews(ids: str):
|
||||
ReviewSegment.delete().where(ReviewSegment.id << list_of_ids).execute()
|
||||
|
||||
return make_response(jsonify({"success": True, "message": "Delete reviews"}), 200)
|
||||
|
||||
|
||||
@ReviewBp.route("/review/activity")
|
||||
def review_activity():
|
||||
"""Get motion and audio activity."""
|
||||
before = request.args.get("before", type=float, default=datetime.now().timestamp())
|
||||
after = request.args.get(
|
||||
"after", type=float, default=(datetime.now() - timedelta(hours=1)).timestamp()
|
||||
)
|
||||
|
||||
# get scale in seconds
|
||||
scale = request.args.get("scale", type=int, default=30)
|
||||
|
||||
all_recordings: list[Recordings] = (
|
||||
Recordings.select(
|
||||
Recordings.start_time,
|
||||
Recordings.duration,
|
||||
Recordings.objects,
|
||||
Recordings.motion,
|
||||
Recordings.dBFS,
|
||||
)
|
||||
.where((Recordings.start_time > after) & (Recordings.end_time < before))
|
||||
.order_by(Recordings.start_time.asc())
|
||||
.iterator()
|
||||
)
|
||||
|
||||
# format is: { timestamp: segment_start_ts, motion: [0-100], audio: [0 - -100] }
|
||||
# periods where active objects / audio was detected will cause motion / audio to be scaled down
|
||||
data: list[dict[str, float]] = []
|
||||
|
||||
for rec in all_recordings:
|
||||
data.append(
|
||||
{
|
||||
"start_time": rec.start_time,
|
||||
"motion": rec.motion if rec.objects == 0 else 0,
|
||||
"audio": rec.dBFS if rec.objects == 0 else 0,
|
||||
}
|
||||
)
|
||||
|
||||
# resample data using pandas to get activity on scaled basis
|
||||
df = pd.DataFrame(data, columns=["start_time", "motion", "audio"])
|
||||
|
||||
# set date as datetime index
|
||||
df["start_time"] = pd.to_datetime(df["start_time"], unit="s")
|
||||
df.set_index(["start_time"], inplace=True)
|
||||
|
||||
# normalize data
|
||||
df = df.resample(f"{scale}S").mean().fillna(0.0)
|
||||
df["motion"] = (
|
||||
(df["motion"] - df["motion"].min())
|
||||
/ (df["motion"].max() - df["motion"].min())
|
||||
* 100
|
||||
)
|
||||
df["audio"] = (
|
||||
(df["audio"] - df["audio"].max())
|
||||
/ (df["audio"].min() - df["audio"].max())
|
||||
* -100
|
||||
)
|
||||
|
||||
# change types for output
|
||||
df.index = df.index.astype(int) // (10**9)
|
||||
normalized = df.reset_index().to_dict("records")
|
||||
return jsonify(normalized)
|
||||
|
||||
Reference in New Issue
Block a user