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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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