mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-07 19:55:26 +03:00
Protect against hour with no cards and ensure data is consistent
This commit is contained in:
parent
211516236b
commit
8b95052768
@ -7,6 +7,7 @@ numpy == 1.23.*
|
||||
onvif_zeep == 0.2.12
|
||||
opencv-python-headless == 4.7.0.*
|
||||
paho-mqtt == 1.6.*
|
||||
pandas == 2.1.4
|
||||
peewee == 3.17.*
|
||||
peewee_migrate == 1.12.*
|
||||
psutil == 5.9.*
|
||||
|
||||
@ -16,6 +16,7 @@ from urllib.parse import unquote
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pytz
|
||||
import requests
|
||||
from flask import (
|
||||
@ -739,41 +740,59 @@ def hourly_timeline_activity(camera_name: str):
|
||||
|
||||
# set initial start so data is representative of full hour
|
||||
hours[int(key.timestamp())].append(
|
||||
{
|
||||
"date": key.timestamp(),
|
||||
"count": 0,
|
||||
"type": "motion",
|
||||
}
|
||||
[
|
||||
key.timestamp(),
|
||||
0,
|
||||
False,
|
||||
]
|
||||
)
|
||||
|
||||
for recording in all_recordings:
|
||||
if recording.start_time > check:
|
||||
hours[int(key.timestamp())].append(
|
||||
{
|
||||
"date": (key + timedelta(hours=1)).timestamp(),
|
||||
"count": 0,
|
||||
"type": "motion",
|
||||
}
|
||||
[
|
||||
(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(
|
||||
{
|
||||
"date": key.timestamp(),
|
||||
"count": 0,
|
||||
"type": "motion",
|
||||
}
|
||||
[
|
||||
key.timestamp(),
|
||||
0,
|
||||
False,
|
||||
]
|
||||
)
|
||||
|
||||
data_type = "motion" if recording.objects == 0 else "objects"
|
||||
data_type = recording.objects > 0
|
||||
hours[int(key.timestamp())].append(
|
||||
{
|
||||
"date": recording.start_time + (recording.duration / 2),
|
||||
"count": recording.motion,
|
||||
"type": data_type,
|
||||
}
|
||||
[
|
||||
recording.start_time + (recording.duration / 2),
|
||||
recording.motion,
|
||||
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["count"] = np.log10(df["count"], where=df["count"] > 0)
|
||||
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,17 +4,34 @@ import Chart from "react-apexcharts";
|
||||
type TimelineGraphProps = {
|
||||
id: string;
|
||||
data: GraphData[];
|
||||
start: number;
|
||||
end: number;
|
||||
objects: number[];
|
||||
};
|
||||
|
||||
/**
|
||||
* A graph meant to be overlaid on top of a timeline
|
||||
*/
|
||||
export default function TimelineGraph({ id, data }: TimelineGraphProps) {
|
||||
export default function TimelineGraph({
|
||||
id,
|
||||
data,
|
||||
start,
|
||||
end,
|
||||
objects,
|
||||
}: TimelineGraphProps) {
|
||||
return (
|
||||
<Chart
|
||||
type="bar"
|
||||
options={{
|
||||
colors: ["#991b1b", "#06b6d4", "#ea580c"],
|
||||
colors: [
|
||||
({ dataPointIndex }: { dataPointIndex: number }) => {
|
||||
if (objects.includes(dataPointIndex)) {
|
||||
return "#06b6d4";
|
||||
} else {
|
||||
return "#991b1b";
|
||||
}
|
||||
},
|
||||
],
|
||||
chart: {
|
||||
id: id,
|
||||
selection: {
|
||||
@ -30,11 +47,27 @@ export default function TimelineGraph({ id, data }: TimelineGraphProps) {
|
||||
dataLabels: { enabled: false },
|
||||
grid: {
|
||||
show: false,
|
||||
padding: {
|
||||
bottom: 20,
|
||||
top: -12,
|
||||
left: -20,
|
||||
right: 0,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
position: "top",
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
columnWidth: "100%",
|
||||
barHeight: "100%",
|
||||
hideZeroBarsWhenGrouped: true,
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
width: 0,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
},
|
||||
@ -49,13 +82,16 @@ export default function TimelineGraph({ id, data }: TimelineGraphProps) {
|
||||
labels: {
|
||||
show: false,
|
||||
},
|
||||
min: start,
|
||||
max: end,
|
||||
},
|
||||
yaxis: {
|
||||
axisBorder: {
|
||||
show: false,
|
||||
},
|
||||
labels: {
|
||||
show: false,
|
||||
},
|
||||
logarithmic: true,
|
||||
logBase: 10,
|
||||
},
|
||||
}}
|
||||
series={data}
|
||||
|
||||
@ -122,7 +122,7 @@ function ConfigEditor() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="absolute top-24 bottom-16 right-0 left-0 md:left-24 lg:left-40">
|
||||
<div className="absolute top-28 bottom-16 right-0 left-0 md:left-24 lg:left-60">
|
||||
<div className="lg:flex justify-between mr-1">
|
||||
<Heading as="h2">Config</Heading>
|
||||
<div>
|
||||
|
||||
@ -26,5 +26,5 @@ type RecordingActivity = {
|
||||
type RecordingSegmentActivity = {
|
||||
date: number;
|
||||
count: number;
|
||||
type: "motion" | "objects";
|
||||
hasObjects: boolean;
|
||||
};
|
||||
|
||||
@ -149,10 +149,8 @@ export function getTimelineHoursForDay(
|
||||
|
||||
end = startDay.getTime() / 1000;
|
||||
const hour = Object.values(day).find((cards) => {
|
||||
if (
|
||||
Object.values(cards)[0].time < start ||
|
||||
Object.values(cards)[0].time > end
|
||||
) {
|
||||
const card = Object.values(cards)[0];
|
||||
if (card == undefined || card.time < start || card.time > end) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@ -185,25 +185,22 @@ export default function DesktopTimelineView({
|
||||
}
|
||||
|
||||
const graphData: {
|
||||
[hour: string]: { objects: GraphDataPoint[]; motion: GraphDataPoint[] };
|
||||
[hour: string]: { objects: number[]; motion: GraphDataPoint[] };
|
||||
} = {};
|
||||
|
||||
Object.entries(activity).forEach(([hour, data]) => {
|
||||
const objects: GraphDataPoint[] = [];
|
||||
const objects: number[] = [];
|
||||
const motion: GraphDataPoint[] = [];
|
||||
|
||||
data.forEach((seg) => {
|
||||
if (seg.type == "objects") {
|
||||
objects.push({
|
||||
x: new Date(seg.date * 1000),
|
||||
y: seg.count,
|
||||
});
|
||||
} else {
|
||||
motion.push({
|
||||
x: new Date(seg.date * 1000),
|
||||
y: seg.count,
|
||||
});
|
||||
data.forEach((seg, idx) => {
|
||||
if (seg.hasObjects) {
|
||||
objects.push(idx);
|
||||
}
|
||||
|
||||
motion.push({
|
||||
x: new Date(seg.date * 1000),
|
||||
y: seg.count,
|
||||
});
|
||||
});
|
||||
|
||||
graphData[hour] = { objects, motion };
|
||||
@ -316,7 +313,7 @@ export default function DesktopTimelineView({
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="m-1 max-h-72 2xl:max-h-80 3xl:max-h-96 overflow-auto">
|
||||
<div className="m-1 w-full max-h-72 2xl:max-h-80 3xl:max-h-96 overflow-auto">
|
||||
{timelineStack.playbackItems.map((timeline) => {
|
||||
const isSelected =
|
||||
timeline.range.start == selectedPlayback.range.start;
|
||||
@ -386,7 +383,7 @@ export default function DesktopTimelineView({
|
||||
doubleClickHandler={() => setSelectedPlayback(timeline)}
|
||||
/>
|
||||
{isSelected && graphData && (
|
||||
<div className="w-full absolute left-0 top-0 h-[84px]">
|
||||
<div className="absolute left-2 right-2 top-0 h-[84px]">
|
||||
<TimelineGraph
|
||||
id={timeline.range.start.toString()}
|
||||
data={[
|
||||
@ -394,8 +391,10 @@ export default function DesktopTimelineView({
|
||||
name: "Motion",
|
||||
data: graphData.motion,
|
||||
},
|
||||
{ name: "Active Objects", data: graphData.objects },
|
||||
]}
|
||||
objects={graphData.objects}
|
||||
start={graphData.motion[0].x.getTime()}
|
||||
end={graphData.motion.at(-1)!!.x.getTime()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user