From 48b672e4b4fa5f7c522d6c59cfe5601db3e8499d Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Mon, 29 Jan 2024 07:47:53 -0700 Subject: [PATCH] Improve scaling of graph motion activity --- frigate/http.py | 4 +- frigate/timeline.py | 9 + web/src/components/graph/TimelineGraph.tsx | 2 +- .../components/scrubber/ActivityScrubber.tsx | 4 +- web/src/utils/historyUtil.ts | 186 ++++++++++-------- web/src/views/history/DesktopTimelineView.tsx | 4 +- 6 files changed, 117 insertions(+), 92 deletions(-) diff --git a/frigate/http.py b/frigate/http.py index f4388a881..0a671e92c 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -784,10 +784,11 @@ def hourly_timeline_activity(camera_name: str): ) data_type = recording.objects > 0 + count = recording.motion + recording.objects hours[int(key.timestamp())].append( [ recording.start_time + (recording.duration / 2), - max(recording.motion, recording.objects), + 0 if count == 0 else np.log2(count), data_type, ] ) @@ -801,7 +802,6 @@ def hourly_timeline_activity(camera_name: str): df.set_index(["date"], inplace=True) # normalize data - df["count"] = np.clip(np.log10(df["count"], where=df["count"] > 0), None, 10) df = df.resample("T").mean().fillna(0) # change types for output diff --git a/frigate/timeline.py b/frigate/timeline.py index 22a02763e..952bb36f4 100644 --- a/frigate/timeline.py +++ b/frigate/timeline.py @@ -108,6 +108,15 @@ class TimelineProcessor(threading.Thread): }, } + # update sub labels for existing entries that haven't been added yet + if ( + prev_event_data != None + and prev_event_data["sub_label"] != event_data["sub_label"] + and event_id in self.pre_event_cache.keys() + ): + for e in self.pre_event_cache[event_id]: + e[Timeline.data]["sub_label"] = event_data["sub_label"] + if event_type == "start": timeline_entry[Timeline.class_type] = "visible" save = True diff --git a/web/src/components/graph/TimelineGraph.tsx b/web/src/components/graph/TimelineGraph.tsx index 3cc7e22e4..73b9b5fb6 100644 --- a/web/src/components/graph/TimelineGraph.tsx +++ b/web/src/components/graph/TimelineGraph.tsx @@ -48,7 +48,7 @@ export default function TimelineGraph({ grid: { show: false, padding: { - bottom: 20, + bottom: 2, top: -12, left: -20, right: 0, diff --git a/web/src/components/scrubber/ActivityScrubber.tsx b/web/src/components/scrubber/ActivityScrubber.tsx index 0171a8a0f..64debc320 100644 --- a/web/src/components/scrubber/ActivityScrubber.tsx +++ b/web/src/components/scrubber/ActivityScrubber.tsx @@ -143,8 +143,8 @@ function ActivityScrubber({ const timelineInstance = new VisTimeline( divElement, - items as DataItem[], - groups as DataGroup[], + (items || []) as DataItem[], + (groups || []) as DataGroup[], timelineOptions ); diff --git a/web/src/utils/historyUtil.ts b/web/src/utils/historyUtil.ts index 5d1467cce..dff634bee 100644 --- a/web/src/utils/historyUtil.ts +++ b/web/src/utils/historyUtil.ts @@ -6,110 +6,124 @@ export function getHourlyTimelineData( detailLevel: string ): CardsData { const cards: CardsData = {}; + const allHours: { [key: string]: Timeline[] } = {}; + timelinePages.forEach((hourlyTimeline) => { - Object.keys(hourlyTimeline["hours"]) - .reverse() - .forEach((hour) => { - const day = new Date(parseInt(hour) * 1000); - day.setHours(0, 0, 0, 0); - const dayKey = (day.getTime() / 1000).toString(); + Object.entries(hourlyTimeline.hours).forEach(([key, values]) => { + if (key in allHours) { + // only occurs when multiple pages contain elements in the same hour + allHours[key] = allHours[key] + .concat(values) + .sort((a, b) => a.timestamp - b.timestamp); + } else { + allHours[key] = values; + } + }); + }); - // build a map of course to the types that are included in this hour - // which allows us to know what items to keep depending on detail level - const sourceToTypes: { [key: string]: string[] } = {}; - let cardTypeStart: { [camera: string]: number } = {}; - Object.values(hourlyTimeline["hours"][hour]).forEach((i) => { - if (i.timestamp > (cardTypeStart[i.camera] ?? 0) + GROUP_SECONDS) { - cardTypeStart[i.camera] = i.timestamp; - } + Object.keys(allHours) + .sort((a, b) => a.localeCompare(b)) + .reverse() + .forEach((hour) => { + const day = new Date(parseInt(hour) * 1000); + day.setHours(0, 0, 0, 0); + const dayKey = (day.getTime() / 1000).toString(); - const groupKey = `${i.source_id}-${cardTypeStart[i.camera]}`; - - if (groupKey in sourceToTypes) { - sourceToTypes[groupKey].push(i.class_type); - } else { - sourceToTypes[groupKey] = [i.class_type]; - } - }); - - if (!(dayKey in cards)) { - cards[dayKey] = {}; + // build a map of course to the types that are included in this hour + // which allows us to know what items to keep depending on detail level + const sourceToTypes: { [key: string]: string[] } = {}; + let cardTypeStart: { [camera: string]: number } = {}; + Object.values(allHours[hour]).forEach((i) => { + if (i.timestamp > (cardTypeStart[i.camera] ?? 0) + GROUP_SECONDS) { + cardTypeStart[i.camera] = i.timestamp; } - if (!(hour in cards[dayKey])) { - cards[dayKey][hour] = {}; + const groupKey = `${i.source_id}-${cardTypeStart[i.camera]}`; + + if (groupKey in sourceToTypes) { + sourceToTypes[groupKey].push(i.class_type); + } else { + sourceToTypes[groupKey] = [i.class_type]; + } + }); + + if (!(dayKey in cards)) { + cards[dayKey] = {}; + } + + if (!(hour in cards[dayKey])) { + cards[dayKey][hour] = {}; + } + + let cardStart: { [camera: string]: number } = {}; + Object.values(allHours[hour]).forEach((i) => { + if (i.timestamp > (cardStart[i.camera] ?? 0) + GROUP_SECONDS) { + cardStart[i.camera] = i.timestamp; } - let cardStart: { [camera: string]: number } = {}; - Object.values(hourlyTimeline["hours"][hour]).forEach((i) => { - if (i.timestamp > (cardStart[i.camera] ?? 0) + GROUP_SECONDS) { - cardStart[i.camera] = i.timestamp; - } + const time = new Date(i.timestamp * 1000); + const groupKey = `${i.camera}-${cardStart[i.camera]}`; + const sourceKey = `${i.source_id}-${cardStart[i.camera]}`; + const uniqueKey = `${i.source_id}-${i.class_type}`; - const time = new Date(i.timestamp * 1000); - const groupKey = `${i.camera}-${cardStart[i.camera]}`; - const sourceKey = `${i.source_id}-${cardStart[i.camera]}`; - const uniqueKey = `${i.source_id}-${i.class_type}`; + // detail level for saving items + // detail level determines which timeline items for each moment is returned + // values can be normal, extra, or full + // normal: return all items except active / attribute / gone / stationary / visible unless that is the only item. + // extra: return all items except attribute / gone / visible unless that is the only item + // full: return all items - // detail level for saving items - // detail level determines which timeline items for each moment is returned - // values can be normal, extra, or full - // normal: return all items except active / attribute / gone / stationary / visible unless that is the only item. - // extra: return all items except attribute / gone / visible unless that is the only item - // full: return all items + let add = true; + const sourceType = sourceToTypes[sourceKey]; + let hiddenItems: string[] = []; + if (detailLevel == "normal") { + hiddenItems = [ + "active", + "attribute", + "gone", + "stationary", + "visible", + ]; + } else if (detailLevel == "extra") { + hiddenItems = ["attribute", "gone", "visible"]; + } - let add = true; - const sourceType = sourceToTypes[sourceKey]; - let hiddenItems: string[] = []; - if (detailLevel == "normal") { - hiddenItems = [ - "active", - "attribute", - "gone", - "stationary", - "visible", - ]; - } else if (detailLevel == "extra") { - hiddenItems = ["attribute", "gone", "visible"]; - } + if (sourceType.length > 1) { + // we have multiple timeline items for this card - if (sourceType.length > 1) { - // we have multiple timeline items for this card - - if ( - sourceType.find((type) => hiddenItems.includes(type) == false) == - undefined - ) { - // all of the attribute items for this card make it hidden, but we need to show one - if (sourceType.indexOf(i.class_type) != 0) { - add = false; - } - } else if (hiddenItems.includes(i.class_type)) { + if ( + sourceType.find((type) => hiddenItems.includes(type) == false) == + undefined + ) { + // all of the attribute items for this card make it hidden, but we need to show one + if (sourceType.indexOf(i.class_type) != 0) { add = false; } + } else if (hiddenItems.includes(i.class_type)) { + add = false; } + } - if (add) { - if (groupKey in cards[dayKey][hour]) { - if ( - !cards[dayKey][hour][groupKey].uniqueKeys.includes(uniqueKey) || - detailLevel == "full" - ) { - cards[dayKey][hour][groupKey].entries.push(i); - cards[dayKey][hour][groupKey].uniqueKeys.push(uniqueKey); - } - } else { - cards[dayKey][hour][groupKey] = { - camera: i.camera, - time: time.getTime() / 1000, - entries: [i], - uniqueKeys: [uniqueKey], - }; + if (add) { + if (groupKey in cards[dayKey][hour]) { + if ( + !cards[dayKey][hour][groupKey].uniqueKeys.includes(uniqueKey) || + detailLevel == "full" + ) { + cards[dayKey][hour][groupKey].entries.push(i); + cards[dayKey][hour][groupKey].uniqueKeys.push(uniqueKey); } + } else { + cards[dayKey][hour][groupKey] = { + camera: i.camera, + time: time.getTime() / 1000, + entries: [i], + uniqueKeys: [uniqueKey], + }; } - }); + } }); - }); + }); return cards; } diff --git a/web/src/views/history/DesktopTimelineView.tsx b/web/src/views/history/DesktopTimelineView.tsx index afff04db7..56be89272 100644 --- a/web/src/views/history/DesktopTimelineView.tsx +++ b/web/src/views/history/DesktopTimelineView.tsx @@ -162,7 +162,6 @@ export default function DesktopTimelineView({ {isSelected ? (
{ controllerRef.current?.scrubToTimestamp(