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