Protect against hour with no cards and ensure data is consistent

This commit is contained in:
Nick Mowen 2024-01-04 07:30:17 -07:00
parent 211516236b
commit 8b95052768
7 changed files with 100 additions and 47 deletions

View File

@ -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.*

View File

@ -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)

View File

@ -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}

View File

@ -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>

View File

@ -26,5 +26,5 @@ type RecordingActivity = {
type RecordingSegmentActivity = {
date: number;
count: number;
type: "motion" | "objects";
hasObjects: boolean;
};

View File

@ -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;
}

View File

@ -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>
)}