mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
Adjustments and fixes (#10346)
* Increase duration of alerts and detections * Add key * Fix cancel button * Fix motion review when switching days * Add reset buttons and make calendar apply immediately * Adjust apis for motion and audio activity * Write review thumbs as webp and reduce size
This commit is contained in:
parent
eeb2187b97
commit
62d13024f6
@ -353,8 +353,8 @@ def delete_reviews(ids: str):
|
|||||||
return make_response(jsonify({"success": True, "message": "Delete reviews"}), 200)
|
return make_response(jsonify({"success": True, "message": "Delete reviews"}), 200)
|
||||||
|
|
||||||
|
|
||||||
@ReviewBp.route("/review/activity")
|
@ReviewBp.route("/review/activity/motion")
|
||||||
def review_activity():
|
def motion_activity():
|
||||||
"""Get motion and audio activity."""
|
"""Get motion and audio activity."""
|
||||||
cameras = request.args.get("cameras", "all")
|
cameras = request.args.get("cameras", "all")
|
||||||
before = request.args.get("before", type=float, default=datetime.now().timestamp())
|
before = request.args.get("before", type=float, default=datetime.now().timestamp())
|
||||||
@ -374,6 +374,68 @@ def review_activity():
|
|||||||
Recordings.duration,
|
Recordings.duration,
|
||||||
Recordings.objects,
|
Recordings.objects,
|
||||||
Recordings.motion,
|
Recordings.motion,
|
||||||
|
)
|
||||||
|
.where(reduce(operator.and_, clauses))
|
||||||
|
.order_by(Recordings.start_time.asc())
|
||||||
|
.iterator()
|
||||||
|
)
|
||||||
|
|
||||||
|
# format is: { timestamp: segment_start_ts, motion: [0-100], audio: [0 - -100] }
|
||||||
|
# periods where active objects / audio was detected will cause motion to be scaled down
|
||||||
|
data: list[dict[str, float]] = []
|
||||||
|
|
||||||
|
for rec in all_recordings:
|
||||||
|
data.append(
|
||||||
|
{
|
||||||
|
"start_time": rec.start_time,
|
||||||
|
"motion": rec.motion if rec.objects == 0 else 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# get scale in seconds
|
||||||
|
scale = request.args.get("scale", type=int, default=30)
|
||||||
|
|
||||||
|
# resample data using pandas to get activity on scaled basis
|
||||||
|
df = pd.DataFrame(data, columns=["start_time", "motion"])
|
||||||
|
|
||||||
|
# set date as datetime index
|
||||||
|
df["start_time"] = pd.to_datetime(df["start_time"], unit="s")
|
||||||
|
df.set_index(["start_time"], inplace=True)
|
||||||
|
|
||||||
|
# normalize data
|
||||||
|
df = df.resample(f"{scale}S").sum().fillna(0.0)
|
||||||
|
df["motion"] = (
|
||||||
|
(df["motion"] - df["motion"].min())
|
||||||
|
/ (df["motion"].max() - df["motion"].min())
|
||||||
|
* 100
|
||||||
|
)
|
||||||
|
|
||||||
|
# change types for output
|
||||||
|
df.index = df.index.astype(int) // (10**9)
|
||||||
|
normalized = df.reset_index().to_dict("records")
|
||||||
|
return jsonify(normalized)
|
||||||
|
|
||||||
|
|
||||||
|
@ReviewBp.route("/review/activity/audio")
|
||||||
|
def audio_activity():
|
||||||
|
"""Get motion and audio activity."""
|
||||||
|
cameras = request.args.get("cameras", "all")
|
||||||
|
before = request.args.get("before", type=float, default=datetime.now().timestamp())
|
||||||
|
after = request.args.get(
|
||||||
|
"after", type=float, default=(datetime.now() - timedelta(hours=1)).timestamp()
|
||||||
|
)
|
||||||
|
|
||||||
|
clauses = [(Recordings.start_time > after) & (Recordings.end_time < before)]
|
||||||
|
|
||||||
|
if cameras != "all":
|
||||||
|
camera_list = cameras.split(",")
|
||||||
|
clauses.append((Recordings.camera << camera_list))
|
||||||
|
|
||||||
|
all_recordings: list[Recordings] = (
|
||||||
|
Recordings.select(
|
||||||
|
Recordings.start_time,
|
||||||
|
Recordings.duration,
|
||||||
|
Recordings.objects,
|
||||||
Recordings.dBFS,
|
Recordings.dBFS,
|
||||||
)
|
)
|
||||||
.where(reduce(operator.and_, clauses))
|
.where(reduce(operator.and_, clauses))
|
||||||
@ -382,14 +444,13 @@ def review_activity():
|
|||||||
)
|
)
|
||||||
|
|
||||||
# format is: { timestamp: segment_start_ts, motion: [0-100], audio: [0 - -100] }
|
# format is: { timestamp: segment_start_ts, motion: [0-100], audio: [0 - -100] }
|
||||||
# periods where active objects / audio was detected will cause motion / audio to be scaled down
|
# periods where active objects / audio was detected will cause audio to be scaled down
|
||||||
data: list[dict[str, float]] = []
|
data: list[dict[str, float]] = []
|
||||||
|
|
||||||
for rec in all_recordings:
|
for rec in all_recordings:
|
||||||
data.append(
|
data.append(
|
||||||
{
|
{
|
||||||
"start_time": rec.start_time,
|
"start_time": rec.start_time,
|
||||||
"motion": rec.motion if rec.objects == 0 else 0,
|
|
||||||
"audio": rec.dBFS if rec.objects == 0 else 0,
|
"audio": rec.dBFS if rec.objects == 0 else 0,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -398,7 +459,7 @@ def review_activity():
|
|||||||
scale = request.args.get("scale", type=int, default=30)
|
scale = request.args.get("scale", type=int, default=30)
|
||||||
|
|
||||||
# resample data using pandas to get activity on scaled basis
|
# resample data using pandas to get activity on scaled basis
|
||||||
df = pd.DataFrame(data, columns=["start_time", "motion", "audio"])
|
df = pd.DataFrame(data, columns=["start_time", "audio"])
|
||||||
|
|
||||||
# set date as datetime index
|
# set date as datetime index
|
||||||
df["start_time"] = pd.to_datetime(df["start_time"], unit="s")
|
df["start_time"] = pd.to_datetime(df["start_time"], unit="s")
|
||||||
@ -406,11 +467,6 @@ def review_activity():
|
|||||||
|
|
||||||
# normalize data
|
# normalize data
|
||||||
df = df.resample(f"{scale}S").mean().fillna(0.0)
|
df = df.resample(f"{scale}S").mean().fillna(0.0)
|
||||||
df["motion"] = (
|
|
||||||
(df["motion"] - df["motion"].min())
|
|
||||||
/ (df["motion"].max() - df["motion"].min())
|
|
||||||
* 100
|
|
||||||
)
|
|
||||||
df["audio"] = (
|
df["audio"] = (
|
||||||
(df["audio"] - df["audio"].max())
|
(df["audio"] - df["audio"].max())
|
||||||
/ (df["audio"].min() - df["audio"].max())
|
/ (df["audio"].min() - df["audio"].max())
|
||||||
|
@ -28,6 +28,10 @@ logger = logging.getLogger(__name__)
|
|||||||
THUMB_HEIGHT = 180
|
THUMB_HEIGHT = 180
|
||||||
THUMB_WIDTH = 320
|
THUMB_WIDTH = 320
|
||||||
|
|
||||||
|
THRESHOLD_ALERT_ACTIVITY = 120
|
||||||
|
THRESHOLD_DETECTION_ACTIVITY = 30
|
||||||
|
THRESHOLD_MOTION_ACTIVITY = 30
|
||||||
|
|
||||||
|
|
||||||
class SeverityEnum(str, Enum):
|
class SeverityEnum(str, Enum):
|
||||||
alert = "alert"
|
alert = "alert"
|
||||||
@ -100,7 +104,7 @@ class PendingReviewSegment:
|
|||||||
path = os.path.join(CLIPS_DIR, f"thumb-{self.camera}-{self.id}.jpg")
|
path = os.path.join(CLIPS_DIR, f"thumb-{self.camera}-{self.id}.jpg")
|
||||||
|
|
||||||
if self.frame is not None:
|
if self.frame is not None:
|
||||||
cv2.imwrite(path, self.frame)
|
cv2.imwrite(path, self.frame, [int(cv2.IMWRITE_WEBP_QUALITY), 60])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ReviewSegment.id: self.id,
|
ReviewSegment.id: self.id,
|
||||||
@ -195,15 +199,16 @@ class ReviewSegmentMaintainer(threading.Thread):
|
|||||||
if len(object["current_zones"]) > 0:
|
if len(object["current_zones"]) > 0:
|
||||||
segment.zones.update(object["current_zones"])
|
segment.zones.update(object["current_zones"])
|
||||||
elif (
|
elif (
|
||||||
segment.severity == SeverityEnum.signification_motion and len(motion) >= 20
|
segment.severity == SeverityEnum.signification_motion
|
||||||
|
and len(motion) >= THRESHOLD_MOTION_ACTIVITY
|
||||||
):
|
):
|
||||||
segment.last_update = frame_time
|
segment.last_update = frame_time
|
||||||
else:
|
else:
|
||||||
if segment.severity == SeverityEnum.alert and frame_time > (
|
if segment.severity == SeverityEnum.alert and frame_time > (
|
||||||
segment.last_update + 60
|
segment.last_update + THRESHOLD_ALERT_ACTIVITY
|
||||||
):
|
):
|
||||||
self.end_segment(segment)
|
self.end_segment(segment)
|
||||||
elif frame_time > (segment.last_update + 10):
|
elif frame_time > (segment.last_update + THRESHOLD_DETECTION_ACTIVITY):
|
||||||
self.end_segment(segment)
|
self.end_segment(segment)
|
||||||
|
|
||||||
def check_if_new_segment(
|
def check_if_new_segment(
|
||||||
|
@ -222,7 +222,7 @@ function NewGroupDialog({ open, setOpen, currentGroups }: NewGroupDialogProps) {
|
|||||||
<DialogContent className="min-w-0 w-96">
|
<DialogContent className="min-w-0 w-96">
|
||||||
<DialogTitle>Camera Groups</DialogTitle>
|
<DialogTitle>Camera Groups</DialogTitle>
|
||||||
{currentGroups.map((group) => (
|
{currentGroups.map((group) => (
|
||||||
<div className="flex justify-between items-center">
|
<div key={group[0]} className="flex justify-between items-center">
|
||||||
{group[0]}
|
{group[0]}
|
||||||
<div className="flex justify-center gap-1">
|
<div className="flex justify-center gap-1">
|
||||||
<Button
|
<Button
|
||||||
|
@ -212,7 +212,7 @@ function CamerasFilterButton({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<div className="flex justify-center items-center">
|
<div className="flex justify-evenly items-center">
|
||||||
<Button
|
<Button
|
||||||
variant="select"
|
variant="select"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -222,6 +222,15 @@ function CamerasFilterButton({
|
|||||||
>
|
>
|
||||||
Apply
|
Apply
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentCameras(undefined);
|
||||||
|
updateCameraFilter(undefined);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -271,8 +280,6 @@ function CalendarFilterButton({
|
|||||||
day,
|
day,
|
||||||
updateSelectedDay,
|
updateSelectedDay,
|
||||||
}: CalendarFilterButtonProps) {
|
}: CalendarFilterButtonProps) {
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [selectedDay, setSelectedDay] = useState(day);
|
|
||||||
const disabledDates = useMemo(() => {
|
const disabledDates = useMemo(() => {
|
||||||
const tomorrow = new Date();
|
const tomorrow = new Date();
|
||||||
tomorrow.setHours(tomorrow.getHours() + 24, -1, 0, 0);
|
tomorrow.setHours(tomorrow.getHours() + 24, -1, 0, 0);
|
||||||
@ -298,22 +305,21 @@ function CalendarFilterButton({
|
|||||||
<Calendar
|
<Calendar
|
||||||
mode="single"
|
mode="single"
|
||||||
disabled={disabledDates}
|
disabled={disabledDates}
|
||||||
selected={selectedDay}
|
selected={day}
|
||||||
showOutsideDays={false}
|
showOutsideDays={false}
|
||||||
onSelect={(day) => {
|
onSelect={(day) => {
|
||||||
setSelectedDay(day);
|
updateSelectedDay(day);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<div className="flex justify-center items-center">
|
<div className="flex justify-center items-center">
|
||||||
<Button
|
<Button
|
||||||
variant="select"
|
variant="secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
updateSelectedDay(selectedDay);
|
updateSelectedDay(undefined);
|
||||||
setOpen(false);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Apply
|
Reset
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@ -321,16 +327,7 @@ function CalendarFilterButton({
|
|||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer>
|
||||||
open={open}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (!open) {
|
|
||||||
setSelectedDay(day);
|
|
||||||
}
|
|
||||||
|
|
||||||
setOpen(open);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
||||||
<DrawerContent>{content}</DrawerContent>
|
<DrawerContent>{content}</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
@ -338,16 +335,7 @@ function CalendarFilterButton({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover>
|
||||||
open={open}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (!open) {
|
|
||||||
setSelectedDay(day);
|
|
||||||
}
|
|
||||||
|
|
||||||
setOpen(open);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
||||||
<PopoverContent>{content}</PopoverContent>
|
<PopoverContent>{content}</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
@ -433,7 +421,7 @@ function GeneralFilterButton({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<div className="flex justify-center items-center">
|
<div className="flex justify-evenly items-center">
|
||||||
<Button
|
<Button
|
||||||
variant="select"
|
variant="select"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -450,6 +438,17 @@ function GeneralFilterButton({
|
|||||||
>
|
>
|
||||||
Apply
|
Apply
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setReviewed(0);
|
||||||
|
setShowReviewed(undefined);
|
||||||
|
setCurrentLabels(undefined);
|
||||||
|
updateLabelFilter(undefined);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -107,7 +107,7 @@ export default function SubmitPlus() {
|
|||||||
alt={`${upload?.label}`}
|
alt={`${upload?.label}`}
|
||||||
/>
|
/>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button>Cancel</Button>
|
<Button onClick={() => setUpload(undefined)}>Cancel</Button>
|
||||||
<Button
|
<Button
|
||||||
className="bg-success"
|
className="bg-success"
|
||||||
onClick={() => onSubmitToPlus(false)}
|
onClick={() => onSubmitToPlus(false)}
|
||||||
|
@ -44,6 +44,6 @@ export type ReviewSummary = {
|
|||||||
|
|
||||||
export type MotionData = {
|
export type MotionData = {
|
||||||
start_time: number;
|
start_time: number;
|
||||||
motion: number;
|
motion?: number;
|
||||||
audio: number;
|
audio?: number;
|
||||||
};
|
};
|
||||||
|
@ -260,6 +260,7 @@ export default function EventView({
|
|||||||
)}
|
)}
|
||||||
{severity == "significant_motion" && (
|
{severity == "significant_motion" && (
|
||||||
<MotionReview
|
<MotionReview
|
||||||
|
key={timeRange.before}
|
||||||
contentRef={contentRef}
|
contentRef={contentRef}
|
||||||
reviewItems={reviewItems}
|
reviewItems={reviewItems}
|
||||||
relevantPreviews={relevantPreviews}
|
relevantPreviews={relevantPreviews}
|
||||||
@ -563,7 +564,7 @@ function MotionReview({
|
|||||||
// motion data
|
// motion data
|
||||||
|
|
||||||
const { data: motionData } = useSWR<MotionData[]>([
|
const { data: motionData } = useSWR<MotionData[]>([
|
||||||
"review/activity",
|
"review/activity/motion",
|
||||||
{
|
{
|
||||||
before: timeRange.before,
|
before: timeRange.before,
|
||||||
after: timeRange.after,
|
after: timeRange.after,
|
||||||
@ -598,7 +599,7 @@ function MotionReview({
|
|||||||
|
|
||||||
const [selectedRangeIdx, setSelectedRangeIdx] = useState(initialIndex);
|
const [selectedRangeIdx, setSelectedRangeIdx] = useState(initialIndex);
|
||||||
const [currentTime, setCurrentTime] = useState<number>(
|
const [currentTime, setCurrentTime] = useState<number>(
|
||||||
startTime ?? timeRangeSegments.ranges[selectedRangeIdx].start,
|
startTime ?? timeRangeSegments.ranges[selectedRangeIdx]?.start,
|
||||||
);
|
);
|
||||||
const currentTimeRange = useMemo(
|
const currentTimeRange = useMemo(
|
||||||
() => timeRangeSegments.ranges[selectedRangeIdx],
|
() => timeRangeSegments.ranges[selectedRangeIdx],
|
||||||
|
Loading…
Reference in New Issue
Block a user