mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-12 08:06:42 +03:00
Compare commits
No commits in common. "6d5098a0c234495cd8fe0474cf9d7768bf9c0d71" and "78d487045b99a7fb687244b2e15d88ed7345c916" have entirely different histories.
6d5098a0c2
...
78d487045b
@ -5,7 +5,7 @@ title: License Plate Recognition (LPR)
|
||||
|
||||
Frigate can recognize license plates on vehicles and automatically add the detected characters to the `recognized_license_plate` field or a known name as a `sub_label` to tracked objects of type `car` or `motorcycle`. A common use case may be to read the license plates of cars pulling into a driveway or cars passing by on a street.
|
||||
|
||||
LPR works best when the license plate is clearly visible to the camera. For moving vehicles, Frigate continuously refines the recognition process, keeping the most confident result. When a vehicle becomes stationary, LPR continues to run for a short time after to attempt recognition.
|
||||
LPR works best when the license plate is clearly visible to the camera. For moving vehicles, Frigate continuously refines the recognition process, keeping the most confident result. However, LPR does not run on stationary vehicles.
|
||||
|
||||
When a plate is recognized, the details are:
|
||||
|
||||
|
||||
@ -4,5 +4,3 @@ from pydantic import BaseModel, conlist, constr
|
||||
class ReviewModifyMultipleBody(BaseModel):
|
||||
# List of string with at least one element and each element with at least one char
|
||||
ids: conlist(constr(min_length=1), min_length=1)
|
||||
# Whether to mark items as reviewed (True) or unreviewed (False)
|
||||
reviewed: bool = True
|
||||
|
||||
@ -435,27 +435,22 @@ async def set_multiple_reviewed(
|
||||
UserReviewStatus.user_id == user_id,
|
||||
UserReviewStatus.review_segment == review_id,
|
||||
)
|
||||
# Update based on the reviewed parameter
|
||||
if review_status.has_been_reviewed != body.reviewed:
|
||||
review_status.has_been_reviewed = body.reviewed
|
||||
# If it exists and isn’t reviewed, update it
|
||||
if not review_status.has_been_reviewed:
|
||||
review_status.has_been_reviewed = True
|
||||
review_status.save()
|
||||
except DoesNotExist:
|
||||
try:
|
||||
UserReviewStatus.create(
|
||||
user_id=user_id,
|
||||
review_segment=ReviewSegment.get(id=review_id),
|
||||
has_been_reviewed=body.reviewed,
|
||||
has_been_reviewed=True,
|
||||
)
|
||||
except (DoesNotExist, IntegrityError):
|
||||
pass
|
||||
|
||||
return JSONResponse(
|
||||
content=(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Marked multiple items as {'reviewed' if body.reviewed else 'unreviewed'}",
|
||||
}
|
||||
),
|
||||
content=({"success": True, "message": "Reviewed multiple items"}),
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
|
||||
@ -48,9 +48,6 @@ class LicensePlateProcessingMixin:
|
||||
MODEL_CACHE_DIR, "paddleocr-onnx", "ppocr_keys_v1.txt"
|
||||
)
|
||||
)
|
||||
# process plates that are stationary and have no position changes for 5 seconds
|
||||
self.stationary_scan_duration = 5
|
||||
|
||||
self.batch_size = 6
|
||||
|
||||
# Object config
|
||||
@ -1272,39 +1269,21 @@ class LicensePlateProcessingMixin:
|
||||
)
|
||||
return
|
||||
|
||||
# don't run for non-stationary objects with no position changes to avoid processing uncertain moving objects
|
||||
# zero position_changes is the initial state after registering a new tracked object
|
||||
# LPR will run 2 frames after detect.min_initialized is reached
|
||||
if obj_data.get("position_changes", 0) == 0 and not obj_data.get(
|
||||
"stationary", False
|
||||
):
|
||||
# don't run for stationary car objects
|
||||
if obj_data.get("stationary") == True:
|
||||
logger.debug(
|
||||
f"{camera}: Skipping LPR for non-stationary {obj_data['label']} object {id} with no position changes. (Detected in {self.config.cameras[camera].detect.min_initialized + 1} concurrent frames, threshold to run is {self.config.cameras[camera].detect.min_initialized + 2} frames)"
|
||||
f"{camera}: Not a processing license plate for a stationary car/motorcycle object."
|
||||
)
|
||||
return
|
||||
|
||||
# run for stationary objects for a limited time after they become stationary
|
||||
if obj_data.get("stationary") == True:
|
||||
threshold = self.config.cameras[camera].detect.stationary.threshold
|
||||
if obj_data.get("motionless_count", 0) >= threshold:
|
||||
frames_since_stationary = (
|
||||
obj_data.get("motionless_count", 0) - threshold
|
||||
)
|
||||
fps = self.config.cameras[camera].detect.fps
|
||||
time_since_stationary = frames_since_stationary / fps
|
||||
|
||||
# only print this log for a short time to avoid log spam
|
||||
if (
|
||||
self.stationary_scan_duration
|
||||
< time_since_stationary
|
||||
<= self.stationary_scan_duration + 1
|
||||
):
|
||||
logger.debug(
|
||||
f"{camera}: {obj_data.get('label', 'An')} object {id} has been stationary for > {self.stationary_scan_duration} seconds, skipping LPR."
|
||||
)
|
||||
|
||||
if time_since_stationary > self.stationary_scan_duration:
|
||||
return
|
||||
# don't run for objects with no position changes
|
||||
# this is the initial state after registering a new tracked object
|
||||
# LPR will run 2 frames after detect.min_initialized is reached
|
||||
if obj_data.get("position_changes", 0) == 0:
|
||||
logger.debug(
|
||||
f"{camera}: Plate detected in {self.config.cameras[camera].detect.min_initialized + 1} concurrent frames, LPR frame threshold ({self.config.cameras[camera].detect.min_initialized + 2})"
|
||||
)
|
||||
return
|
||||
|
||||
license_plate: Optional[dict[str, Any]] = None
|
||||
|
||||
@ -1445,7 +1424,7 @@ class LicensePlateProcessingMixin:
|
||||
license_plate_frame,
|
||||
)
|
||||
|
||||
logger.debug(f"{camera}: Running plate recognition for id: {id}.")
|
||||
logger.debug(f"{camera}: Running plate recognition.")
|
||||
|
||||
# run detection, returns results sorted by confidence, best first
|
||||
start = datetime.datetime.now().timestamp()
|
||||
|
||||
@ -416,7 +416,7 @@ class TestHttpReview(BaseTestHttp):
|
||||
assert response.status_code == 200
|
||||
response = response.json()
|
||||
assert response["success"] == True
|
||||
assert response["message"] == "Marked multiple items as reviewed"
|
||||
assert response["message"] == "Reviewed multiple items"
|
||||
# Verify that in DB the review segment was not changed
|
||||
with self.assertRaises(DoesNotExist):
|
||||
UserReviewStatus.get(
|
||||
@ -433,7 +433,7 @@ class TestHttpReview(BaseTestHttp):
|
||||
assert response.status_code == 200
|
||||
response_json = response.json()
|
||||
assert response_json["success"] == True
|
||||
assert response_json["message"] == "Marked multiple items as reviewed"
|
||||
assert response_json["message"] == "Reviewed multiple items"
|
||||
# Verify UserReviewStatus was created
|
||||
user_review = UserReviewStatus.get(
|
||||
UserReviewStatus.user_id == self.user_id,
|
||||
|
||||
@ -106,7 +106,6 @@
|
||||
"button": {
|
||||
"export": "Export",
|
||||
"markAsReviewed": "Mark as reviewed",
|
||||
"markAsUnreviewed": "Mark as unreviewed",
|
||||
"deleteNow": "Delete Now"
|
||||
}
|
||||
},
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
import { FaCircleCheck, FaCircleXmark } from "react-icons/fa6";
|
||||
import { FaCircleCheck } from "react-icons/fa6";
|
||||
import { useCallback, useState } from "react";
|
||||
import axios from "axios";
|
||||
import { Button, buttonVariants } from "../ui/button";
|
||||
import { isDesktop } from "react-device-detect";
|
||||
import { FaCompactDisc } from "react-icons/fa";
|
||||
import { HiTrash } from "react-icons/hi";
|
||||
import { ReviewSegment } from "@/types/review";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@ -21,8 +20,8 @@ import { Trans, useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
|
||||
type ReviewActionGroupProps = {
|
||||
selectedReviews: ReviewSegment[];
|
||||
setSelectedReviews: (reviews: ReviewSegment[]) => void;
|
||||
selectedReviews: string[];
|
||||
setSelectedReviews: (ids: string[]) => void;
|
||||
onExport: (id: string) => void;
|
||||
pullLatestData: () => void;
|
||||
};
|
||||
@ -37,24 +36,15 @@ export default function ReviewActionGroup({
|
||||
setSelectedReviews([]);
|
||||
}, [setSelectedReviews]);
|
||||
|
||||
const allReviewed = selectedReviews.every(
|
||||
(review) => review.has_been_reviewed,
|
||||
);
|
||||
|
||||
const onToggleReviewed = useCallback(async () => {
|
||||
const ids = selectedReviews.map((review) => review.id);
|
||||
await axios.post(`reviews/viewed`, {
|
||||
ids,
|
||||
reviewed: !allReviewed,
|
||||
});
|
||||
const onMarkAsReviewed = useCallback(async () => {
|
||||
await axios.post(`reviews/viewed`, { ids: selectedReviews });
|
||||
setSelectedReviews([]);
|
||||
pullLatestData();
|
||||
}, [selectedReviews, setSelectedReviews, pullLatestData, allReviewed]);
|
||||
}, [selectedReviews, setSelectedReviews, pullLatestData]);
|
||||
|
||||
const onDelete = useCallback(() => {
|
||||
const ids = selectedReviews.map((review) => review.id);
|
||||
axios
|
||||
.post(`reviews/delete`, { ids })
|
||||
.post(`reviews/delete`, { ids: selectedReviews })
|
||||
.then((resp) => {
|
||||
if (resp.status === 200) {
|
||||
toast.success(t("recording.confirmDelete.toast.success"), {
|
||||
@ -150,7 +140,7 @@ export default function ReviewActionGroup({
|
||||
aria-label={t("recording.button.export")}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
onExport(selectedReviews[0].id);
|
||||
onExport(selectedReviews[0]);
|
||||
onClearSelected();
|
||||
}}
|
||||
>
|
||||
@ -164,24 +154,14 @@ export default function ReviewActionGroup({
|
||||
)}
|
||||
<Button
|
||||
className="flex items-center gap-2 p-2"
|
||||
aria-label={
|
||||
allReviewed
|
||||
? t("recording.button.markAsUnreviewed")
|
||||
: t("recording.button.markAsReviewed")
|
||||
}
|
||||
aria-label={t("recording.button.markAsReviewed")}
|
||||
size="sm"
|
||||
onClick={onToggleReviewed}
|
||||
onClick={onMarkAsReviewed}
|
||||
>
|
||||
{allReviewed ? (
|
||||
<FaCircleXmark className="text-secondary-foreground" />
|
||||
) : (
|
||||
<FaCircleCheck className="text-secondary-foreground" />
|
||||
)}
|
||||
<FaCircleCheck className="text-secondary-foreground" />
|
||||
{isDesktop && (
|
||||
<div className="text-primary">
|
||||
{allReviewed
|
||||
? t("recording.button.markAsUnreviewed")
|
||||
: t("recording.button.markAsReviewed")}
|
||||
{t("recording.button.markAsReviewed")}
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@ -356,7 +356,6 @@ export default function Events() {
|
||||
if (itemsToMarkReviewed.length > 0) {
|
||||
await axios.post(`reviews/viewed`, {
|
||||
ids: itemsToMarkReviewed,
|
||||
reviewed: true,
|
||||
});
|
||||
reloadData();
|
||||
}
|
||||
@ -366,10 +365,7 @@ export default function Events() {
|
||||
|
||||
const markItemAsReviewed = useCallback(
|
||||
async (review: ReviewSegment) => {
|
||||
const resp = await axios.post(`reviews/viewed`, {
|
||||
ids: [review.id],
|
||||
reviewed: true,
|
||||
});
|
||||
const resp = await axios.post(`reviews/viewed`, { ids: [review.id] });
|
||||
|
||||
if (resp.status == 200) {
|
||||
updateSegments(
|
||||
|
||||
@ -135,11 +135,11 @@ export default function EventView({
|
||||
|
||||
// review interaction
|
||||
|
||||
const [selectedReviews, setSelectedReviews] = useState<ReviewSegment[]>([]);
|
||||
const [selectedReviews, setSelectedReviews] = useState<string[]>([]);
|
||||
const onSelectReview = useCallback(
|
||||
(review: ReviewSegment, ctrl: boolean) => {
|
||||
if (selectedReviews.length > 0 || ctrl) {
|
||||
const index = selectedReviews.findIndex((r) => r.id === review.id);
|
||||
const index = selectedReviews.indexOf(review.id);
|
||||
|
||||
if (index != -1) {
|
||||
if (selectedReviews.length == 1) {
|
||||
@ -153,7 +153,7 @@ export default function EventView({
|
||||
}
|
||||
} else {
|
||||
const copy = [...selectedReviews];
|
||||
copy.push(review);
|
||||
copy.push(review.id);
|
||||
setSelectedReviews(copy);
|
||||
}
|
||||
} else {
|
||||
@ -175,7 +175,7 @@ export default function EventView({
|
||||
}
|
||||
|
||||
if (selectedReviews.length < currentReviewItems.length) {
|
||||
setSelectedReviews(currentReviewItems);
|
||||
setSelectedReviews(currentReviewItems.map((seg) => seg.id));
|
||||
} else {
|
||||
setSelectedReviews([]);
|
||||
}
|
||||
@ -429,7 +429,7 @@ type DetectionReviewProps = {
|
||||
currentItems: ReviewSegment[] | null;
|
||||
itemsToReview?: number;
|
||||
relevantPreviews?: Preview[];
|
||||
selectedReviews: ReviewSegment[];
|
||||
selectedReviews: string[];
|
||||
severity: ReviewSeverity;
|
||||
filter?: ReviewFilter;
|
||||
timeRange: { before: number; after: number };
|
||||
@ -439,7 +439,7 @@ type DetectionReviewProps = {
|
||||
markAllItemsAsReviewed: (currentItems: ReviewSegment[]) => void;
|
||||
onSelectReview: (review: ReviewSegment, ctrl: boolean) => void;
|
||||
onSelectAllReviews: () => void;
|
||||
setSelectedReviews: (reviews: ReviewSegment[]) => void;
|
||||
setSelectedReviews: (reviewIds: string[]) => void;
|
||||
pullLatestData: () => void;
|
||||
};
|
||||
function DetectionReview({
|
||||
@ -667,7 +667,7 @@ function DetectionReview({
|
||||
case "r":
|
||||
if (selectedReviews.length > 0 && !modifiers.repeat) {
|
||||
currentItems?.forEach((item) => {
|
||||
if (selectedReviews.some((r) => r.id === item.id)) {
|
||||
if (selectedReviews.includes(item.id)) {
|
||||
item.has_been_reviewed = true;
|
||||
markItemAsReviewed(item);
|
||||
}
|
||||
@ -723,7 +723,7 @@ function DetectionReview({
|
||||
>
|
||||
{!loading && currentItems
|
||||
? currentItems.map((value) => {
|
||||
const selected = selectedReviews.some((r) => r.id === value.id);
|
||||
const selected = selectedReviews.includes(value.id);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
Loading…
Reference in New Issue
Block a user