Rename identifier field (#17128)

* backend rename

* frontend

* docs

* fix api path
This commit is contained in:
Josh Hawkins 2025-03-12 18:45:16 -05:00 committed by GitHub
parent b7333557a1
commit 124cc4c9cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 238 additions and 158 deletions

View File

@ -3,16 +3,16 @@ id: license_plate_recognition
title: License Plate Recognition (LPR) title: License Plate Recognition (LPR)
--- ---
Frigate can recognize license plates on vehicles and automatically add the detected characters to the `identifier` field or a known name as a `sub_label` to objects that are of type `car`. A common use case may be to read the license plates of cars pulling into a driveway or cars passing by on a street. 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 objects that are of type `car`. 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. However, LPR does not run on stationary vehicles. 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 recognized name is: When a plate is recognized, the recognized name is:
- Added to the `car` tracked object as a `sub_label` (if known) or the `identifier` field (if unknown) - Added to the `car` tracked object as a `sub_label` (if known) or the `recognized_license_plate` field (if unknown)
- Viewable in the Review Item Details pane in Review and the Tracked Object Details pane in Explore. - Viewable in the Review Item Details pane in Review and the Tracked Object Details pane in Explore.
- Filterable through the More Filters menu in Explore. - Filterable through the More Filters menu in Explore.
- Published via the `frigate/events` MQTT topic as a `sub_label` (known) or `identifier` (unknown) for the tracked object. - Published via the `frigate/events` MQTT topic as a `sub_label` (known) or `recognized_license_plate` (unknown) for the tracked object.
## Model Requirements ## Model Requirements
@ -71,7 +71,7 @@ Fine-tune the LPR feature using these optional parameters:
- **`known_plates`**: List of strings or regular expressions that assign custom a `sub_label` to `car` objects when a recognized plate matches a known value. - **`known_plates`**: List of strings or regular expressions that assign custom a `sub_label` to `car` objects when a recognized plate matches a known value.
- These labels appear in the UI, filters, and notifications. - These labels appear in the UI, filters, and notifications.
- Unknown plates are still saved but are added to the `identifier` field rather than the `sub_label`. - Unknown plates are still saved but are added to the `recognized_license_plate` field rather than the `sub_label`.
- **`match_distance`**: Allows for minor variations (missing/incorrect characters) when matching a detected plate to a known plate. - **`match_distance`**: Allows for minor variations (missing/incorrect characters) when matching a detected plate to a known plate.
- For example, setting `match_distance: 1` allows a plate `ABCDE` to match `ABCBE` or `ABCD`. - For example, setting `match_distance: 1` allows a plate `ABCDE` to match `ABCBE` or `ABCD`.
- This parameter will _not_ operate on known plates that are defined as regular expressions. You should define the full string of your plate in `known_plates` in order to use `match_distance`. - This parameter will _not_ operate on known plates that are defined as regular expressions. You should define the full string of your plate in `known_plates` in order to use `match_distance`.

View File

@ -55,8 +55,8 @@ Message published for each changed tracked object. The first message is publishe
"current_attributes": [], // detailed data about the current attributes in this frame "current_attributes": [], // detailed data about the current attributes in this frame
"current_estimated_speed": 0.71, // current estimated speed (mph or kph) for objects moving through zones with speed estimation enabled "current_estimated_speed": 0.71, // current estimated speed (mph or kph) for objects moving through zones with speed estimation enabled
"velocity_angle": 180, // direction of travel relative to the frame for objects moving through zones with speed estimation enabled "velocity_angle": 180, // direction of travel relative to the frame for objects moving through zones with speed estimation enabled
"identifier": "ABC12345", // an identifier for this object - in this case, an unrecognized license plate "recognized_license_plate": "ABC12345", // a recognized license plate for car objects
"identifier_score": 0.933451 "recognized_license_plate_score": 0.933451
}, },
"after": { "after": {
"id": "1607123955.475377-mxklsc", "id": "1607123955.475377-mxklsc",
@ -96,8 +96,8 @@ Message published for each changed tracked object. The first message is publishe
], ],
"current_estimated_speed": 0.77, // current estimated speed (mph or kph) for objects moving through zones with speed estimation enabled "current_estimated_speed": 0.77, // current estimated speed (mph or kph) for objects moving through zones with speed estimation enabled
"velocity_angle": 180, // direction of travel relative to the frame for objects moving through zones with speed estimation enabled "velocity_angle": 180, // direction of travel relative to the frame for objects moving through zones with speed estimation enabled
"identifier": "ABC12345", // an identifier for this object - in this case, an unrecognized license plate "recognized_license_plate": "ABC12345", // a recognized license plate for car objects
"identifier_score": 0.933451 "recognized_license_plate_score": 0.933451
} }
} }
``` ```

View File

@ -619,37 +619,39 @@ def get_sub_labels(split_joined: Optional[int] = None):
return JSONResponse(content=sub_labels) return JSONResponse(content=sub_labels)
@router.get("/identifiers") @router.get("/recognized_license_plates")
def get_identifiers(split_joined: Optional[int] = None): def get_recognized_license_plates(split_joined: Optional[int] = None):
try: try:
events = Event.select(Event.data).distinct() events = Event.select(Event.data).distinct()
except Exception: except Exception:
return JSONResponse( return JSONResponse(
content=({"success": False, "message": "Failed to get identifiers"}), content=(
{"success": False, "message": "Failed to get recognized license plates"}
),
status_code=404, status_code=404,
) )
identifiers = [] recognized_license_plates = []
for e in events: for e in events:
if e.data is not None and "identifier" in e.data: if e.data is not None and "recognized_license_plate" in e.data:
identifiers.append(e.data["identifier"]) recognized_license_plates.append(e.data["recognized_license_plate"])
while None in identifiers: while None in recognized_license_plates:
identifiers.remove(None) recognized_license_plates.remove(None)
if split_joined: if split_joined:
original_identifiers = identifiers.copy() original_recognized_license_plates = recognized_license_plates.copy()
for identifier in original_identifiers: for recognized_license_plate in original_recognized_license_plates:
if identifier and "," in identifier: if recognized_license_plate and "," in recognized_license_plate:
identifiers.remove(identifier) recognized_license_plates.remove(recognized_license_plate)
parts = identifier.split(",") parts = recognized_license_plate.split(",")
for part in parts: for part in parts:
if part.strip() not in identifiers: if part.strip() not in recognized_license_plates:
identifiers.append(part.strip()) recognized_license_plates.append(part.strip())
identifiers = list(set(identifiers)) recognized_license_plates = list(set(recognized_license_plates))
identifiers.sort() recognized_license_plates.sort()
return JSONResponse(content=identifiers) return JSONResponse(content=recognized_license_plates)
@router.get("/timeline") @router.get("/timeline")

View File

@ -27,7 +27,7 @@ class EventsQueryParams(BaseModel):
max_score: Optional[float] = None max_score: Optional[float] = None
min_speed: Optional[float] = None min_speed: Optional[float] = None
max_speed: Optional[float] = None max_speed: Optional[float] = None
identifier: Optional[str] = "all" recognized_license_plate: Optional[str] = "all"
is_submitted: Optional[int] = None is_submitted: Optional[int] = None
min_length: Optional[float] = None min_length: Optional[float] = None
max_length: Optional[float] = None max_length: Optional[float] = None
@ -56,7 +56,7 @@ class EventsSearchQueryParams(BaseModel):
max_score: Optional[float] = None max_score: Optional[float] = None
min_speed: Optional[float] = None min_speed: Optional[float] = None
max_speed: Optional[float] = None max_speed: Optional[float] = None
identifier: Optional[str] = "all" recognized_license_plate: Optional[str] = "all"
sort: Optional[str] = None sort: Optional[str] = None

View File

@ -101,7 +101,7 @@ def events(params: EventsQueryParams = Depends()):
min_length = params.min_length min_length = params.min_length
max_length = params.max_length max_length = params.max_length
event_id = params.event_id event_id = params.event_id
identifier = params.identifier recognized_license_plate = params.recognized_license_plate
sort = params.sort sort = params.sort
@ -159,31 +159,44 @@ def events(params: EventsQueryParams = Depends()):
sub_label_clause = reduce(operator.or_, sub_label_clauses) sub_label_clause = reduce(operator.or_, sub_label_clauses)
clauses.append((sub_label_clause)) clauses.append((sub_label_clause))
if identifier != "all": if recognized_license_plate != "all":
# use matching so joined identifiers are included # use matching so joined recognized_license_plates are included
# for example an identifier 'ABC123' would get events # for example a recognized license plate 'ABC123' would get events
# with identifiers 'ABC123' and 'ABC123, XYZ789' # with recognized license plates 'ABC123' and 'ABC123, XYZ789'
identifier_clauses = [] recognized_license_plate_clauses = []
filtered_identifiers = identifier.split(",") filtered_recognized_license_plates = recognized_license_plate.split(",")
if "None" in filtered_identifiers: if "None" in filtered_recognized_license_plates:
filtered_identifiers.remove("None") filtered_recognized_license_plates.remove("None")
identifier_clauses.append((Event.data["identifier"].is_null())) recognized_license_plate_clauses.append(
(Event.data["recognized_license_plate"].is_null())
)
for identifier in filtered_identifiers: for recognized_license_plate in filtered_recognized_license_plates:
# Exact matching plus list inclusion # Exact matching plus list inclusion
identifier_clauses.append( recognized_license_plate_clauses.append(
(Event.data["identifier"].cast("text") == identifier) (
Event.data["recognized_license_plate"].cast("text")
== recognized_license_plate
)
) )
identifier_clauses.append( recognized_license_plate_clauses.append(
(Event.data["identifier"].cast("text") % f"*{identifier},*") (
Event.data["recognized_license_plate"].cast("text")
% f"*{recognized_license_plate},*"
)
) )
identifier_clauses.append( recognized_license_plate_clauses.append(
(Event.data["identifier"].cast("text") % f"*, {identifier}*") (
Event.data["recognized_license_plate"].cast("text")
% f"*, {recognized_license_plate}*"
)
) )
identifier_clause = reduce(operator.or_, identifier_clauses) recognized_license_plate_clause = reduce(
clauses.append((identifier_clause)) operator.or_, recognized_license_plate_clauses
)
clauses.append((recognized_license_plate_clause))
if zones != "all": if zones != "all":
# use matching so events with multiple zones # use matching so events with multiple zones
@ -367,8 +380,8 @@ def events_explore(limit: int = 10):
"average_estimated_speed", "average_estimated_speed",
"velocity_angle", "velocity_angle",
"path_data", "path_data",
"identifier", "recognized_license_plate",
"identifier_score", "recognized_license_plate_score",
] ]
}, },
"event_count": label_counts[event.label], "event_count": label_counts[event.label],
@ -426,7 +439,7 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends())
has_clip = params.has_clip has_clip = params.has_clip
has_snapshot = params.has_snapshot has_snapshot = params.has_snapshot
is_submitted = params.is_submitted is_submitted = params.is_submitted
identifier = params.identifier recognized_license_plate = params.recognized_license_plate
# for similarity search # for similarity search
event_id = params.event_id event_id = params.event_id
@ -496,31 +509,44 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends())
event_filters.append((reduce(operator.or_, zone_clauses))) event_filters.append((reduce(operator.or_, zone_clauses)))
if identifier != "all": if recognized_license_plate != "all":
# use matching so joined identifiers are included # use matching so joined recognized_license_plates are included
# for example an identifier 'ABC123' would get events # for example an recognized_license_plate 'ABC123' would get events
# with identifiers 'ABC123' and 'ABC123, XYZ789' # with recognized_license_plates 'ABC123' and 'ABC123, XYZ789'
identifier_clauses = [] recognized_license_plate_clauses = []
filtered_identifiers = identifier.split(",") filtered_recognized_license_plates = recognized_license_plate.split(",")
if "None" in filtered_identifiers: if "None" in filtered_recognized_license_plates:
filtered_identifiers.remove("None") filtered_recognized_license_plates.remove("None")
identifier_clauses.append((Event.data["identifier"].is_null())) recognized_license_plate_clauses.append(
(Event.data["recognized_license_plate"].is_null())
)
for identifier in filtered_identifiers: for recognized_license_plate in filtered_recognized_license_plates:
# Exact matching plus list inclusion # Exact matching plus list inclusion
identifier_clauses.append( recognized_license_plate_clauses.append(
(Event.data["identifier"].cast("text") == identifier) (
Event.data["recognized_license_plate"].cast("text")
== recognized_license_plate
)
) )
identifier_clauses.append( recognized_license_plate_clauses.append(
(Event.data["identifier"].cast("text") % f"*{identifier},*") (
Event.data["recognized_license_plate"].cast("text")
% f"*{recognized_license_plate},*"
)
) )
identifier_clauses.append( recognized_license_plate_clauses.append(
(Event.data["identifier"].cast("text") % f"*, {identifier}*") (
Event.data["recognized_license_plate"].cast("text")
% f"*, {recognized_license_plate}*"
)
) )
identifier_clause = reduce(operator.or_, identifier_clauses) recognized_license_plate_clause = reduce(
event_filters.append((identifier_clause)) operator.or_, recognized_license_plate_clauses
)
event_filters.append((recognized_license_plate_clause))
if after: if after:
event_filters.append((Event.start_time > after)) event_filters.append((Event.start_time > after))
@ -683,8 +709,8 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends())
"average_estimated_speed", "average_estimated_speed",
"velocity_angle", "velocity_angle",
"path_data", "path_data",
"identifier", "recognized_license_plate",
"identifier_score", "recognized_license_plate_score",
] ]
} }

View File

@ -141,8 +141,11 @@ class CameraState:
if ( if (
obj.get("sub_label") and is_label_printable(obj["sub_label"][0]) obj.get("sub_label") and is_label_printable(obj["sub_label"][0])
) )
else obj.get("identifier", [None])[0] else obj.get("recognized_license_plate", [None])[0]
if (obj.get("identifier") and obj["identifier"][0]) if (
obj.get("recognized_license_plate")
and obj["recognized_license_plate"][0]
)
else obj["label"] else obj["label"]
) )
draw_box_with_label( draw_box_with_label(

View File

@ -14,7 +14,7 @@ class EventMetadataTypeEnum(str, Enum):
manual_event_end = "manual_event_end" manual_event_end = "manual_event_end"
regenerate_description = "regenerate_description" regenerate_description = "regenerate_description"
sub_label = "sub_label" sub_label = "sub_label"
identifier = "identifier" recognized_license_plate = "recognized_license_plate"
class EventMetadataPublisher(Publisher): class EventMetadataPublisher(Publisher):

View File

@ -1064,7 +1064,8 @@ class LicensePlateProcessingMixin:
) )
self.sub_label_publisher.publish( self.sub_label_publisher.publish(
EventMetadataTypeEnum.identifier, (id, top_plate, avg_confidence) EventMetadataTypeEnum.recognized_license_plate,
(id, top_plate, avg_confidence),
) )
self.detected_license_plates[id] = { self.detected_license_plates[id] = {

View File

@ -27,7 +27,8 @@ def should_update_db(prev_event: Event, current_event: Event) -> bool:
or prev_event["average_estimated_speed"] or prev_event["average_estimated_speed"]
!= current_event["average_estimated_speed"] != current_event["average_estimated_speed"]
or prev_event["velocity_angle"] != current_event["velocity_angle"] or prev_event["velocity_angle"] != current_event["velocity_angle"]
or prev_event["identifier"] != current_event["identifier"] or prev_event["recognized_license_plate"]
!= current_event["recognized_license_plate"]
or prev_event["path_data"] != current_event["path_data"] or prev_event["path_data"] != current_event["path_data"]
): ):
return True return True
@ -227,10 +228,14 @@ class EventProcessor(threading.Thread):
event[Event.sub_label] = event_data["sub_label"][0] event[Event.sub_label] = event_data["sub_label"][0]
event[Event.data]["sub_label_score"] = event_data["sub_label"][1] event[Event.data]["sub_label_score"] = event_data["sub_label"][1]
# only overwrite the identifier in the database if it's set # only overwrite the recognized_license_plate in the database if it's set
if event_data.get("identifier") is not None: if event_data.get("recognized_license_plate") is not None:
event[Event.data]["identifier"] = event_data["identifier"][0] event[Event.data]["recognized_license_plate"] = event_data[
event[Event.data]["identifier_score"] = event_data["identifier"][1] "recognized_license_plate"
][0]
event[Event.data]["recognized_license_plate_score"] = event_data[
"recognized_license_plate"
][1]
( (
Event.insert(event) Event.insert(event)

View File

@ -346,10 +346,10 @@ class TrackedObjectProcessor(threading.Thread):
return True return True
def set_identifier( def set_recognized_license_plate(
self, event_id: str, identifier: str | None, score: float | None self, event_id: str, recognized_license_plate: str | None, score: float | None
) -> None: ) -> None:
"""Update identifier for given event id.""" """Update recognized license plate for given event id."""
tracked_obj: TrackedObject = None tracked_obj: TrackedObject = None
for state in self.camera_states.values(): for state in self.camera_states.values():
@ -367,15 +367,18 @@ class TrackedObjectProcessor(threading.Thread):
return return
if tracked_obj: if tracked_obj:
tracked_obj.obj_data["identifier"] = (identifier, score) tracked_obj.obj_data["recognized_license_plate"] = (
recognized_license_plate,
score,
)
if event: if event:
data = event.data data = event.data
data["identifier"] = identifier data["recognized_license_plate"] = recognized_license_plate
if identifier is None: if recognized_license_plate is None:
data["identifier_score"] = None data["recognized_license_plate_score"] = None
elif score is not None: elif score is not None:
data["identifier_score"] = score data["recognized_license_plate_score"] = score
event.data = data event.data = data
event.save() event.save()
@ -542,9 +545,11 @@ class TrackedObjectProcessor(threading.Thread):
if topic.endswith(EventMetadataTypeEnum.sub_label.value): if topic.endswith(EventMetadataTypeEnum.sub_label.value):
(event_id, sub_label, score) = payload (event_id, sub_label, score) = payload
self.set_sub_label(event_id, sub_label, score) self.set_sub_label(event_id, sub_label, score)
if topic.endswith(EventMetadataTypeEnum.identifier.value): if topic.endswith(EventMetadataTypeEnum.recognized_license_plate.value):
(event_id, identifier, score) = payload (event_id, recognized_license_plate, score) = payload
self.set_identifier(event_id, identifier, score) self.set_recognized_license_plate(
event_id, recognized_license_plate, score
)
elif topic.endswith(EventMetadataTypeEnum.manual_event_create.value): elif topic.endswith(EventMetadataTypeEnum.manual_event_create.value):
self.create_manual_event(payload) self.create_manual_event(payload)
elif topic.endswith(EventMetadataTypeEnum.manual_event_end.value): elif topic.endswith(EventMetadataTypeEnum.manual_event_end.value):

View File

@ -153,8 +153,12 @@ class TrackedObject:
"current_estimated_speed": self.current_estimated_speed, "current_estimated_speed": self.current_estimated_speed,
"velocity_angle": self.velocity_angle, "velocity_angle": self.velocity_angle,
"path_data": self.path_data, "path_data": self.path_data,
"identifier": obj_data.get("identifier"), "recognized_license_plate": obj_data.get(
"identifier_score": obj_data.get("identifier_score"), "recognized_license_plate"
),
"recognized_license_plate_score": obj_data.get(
"recognized_license_plate_score"
),
} }
thumb_update = True thumb_update = True
@ -367,7 +371,7 @@ class TrackedObject:
"average_estimated_speed": self.average_estimated_speed, "average_estimated_speed": self.average_estimated_speed,
"velocity_angle": self.velocity_angle, "velocity_angle": self.velocity_angle,
"path_data": self.path_data, "path_data": self.path_data,
"identifier": self.obj_data.get("identifier"), "recognized_license_plate": self.obj_data.get("recognized_license_plate"),
} }
return event return event

View File

@ -333,13 +333,18 @@ function ObjectDetailsTab({
} }
}, [search]); }, [search]);
const identifierScore = useMemo(() => { const recognizedLicensePlateScore = useMemo(() => {
if (!search) { if (!search) {
return undefined; return undefined;
} }
if (search.data.identifier && search.data?.identifier_score) { if (
return Math.round((search.data?.identifier_score ?? 0) * 100); search.data.recognized_license_plate &&
search.data?.recognized_license_plate_score
) {
return Math.round(
(search.data?.recognized_license_plate_score ?? 0) * 100,
);
} else { } else {
return undefined; return undefined;
} }
@ -550,13 +555,16 @@ function ObjectDetailsTab({
</Tooltip> </Tooltip>
</div> </div>
</div> </div>
{search?.data.identifier && ( {search?.data.recognized_license_plate && (
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<div className="text-sm text-primary/40">Identifier</div> <div className="text-sm text-primary/40">
Recognized License Plate
</div>
<div className="flex flex-col space-y-0.5 text-sm"> <div className="flex flex-col space-y-0.5 text-sm">
<div className="flex flex-row items-center gap-2"> <div className="flex flex-row items-center gap-2">
{search.data.identifier}{" "} {search.data.recognized_license_plate}{" "}
{identifierScore && ` (${identifierScore}%)`} {recognizedLicensePlateScore &&
` (${recognizedLicensePlateScore}%)`}
</div> </div>
</div> </div>
</div> </div>

View File

@ -86,7 +86,7 @@ export default function SearchFilterDialog({
(currentFilter.max_speed ?? 150) < 150 || (currentFilter.max_speed ?? 150) < 150 ||
(currentFilter.zones?.length ?? 0) > 0 || (currentFilter.zones?.length ?? 0) > 0 ||
(currentFilter.sub_labels?.length ?? 0) > 0 || (currentFilter.sub_labels?.length ?? 0) > 0 ||
(currentFilter.identifier?.length ?? 0) > 0), (currentFilter.recognized_license_plate?.length ?? 0) > 0),
[currentFilter], [currentFilter],
); );
@ -128,10 +128,13 @@ export default function SearchFilterDialog({
setCurrentFilter({ ...currentFilter, sub_labels: newSubLabels }) setCurrentFilter({ ...currentFilter, sub_labels: newSubLabels })
} }
/> />
<IdentifierFilterContent <RecognizedLicensePlatesFilterContent
identifiers={currentFilter.identifier} recognizedLicensePlates={currentFilter.recognized_license_plate}
setIdentifiers={(identifiers) => setRecognizedLicensePlates={(plate) =>
setCurrentFilter({ ...currentFilter, identifier: identifiers }) setCurrentFilter({
...currentFilter,
recognized_license_plate: plate,
})
} }
/> />
<ScoreFilterContent <ScoreFilterContent
@ -207,7 +210,7 @@ export default function SearchFilterDialog({
max_speed: undefined, max_speed: undefined,
has_snapshot: undefined, has_snapshot: undefined,
has_clip: undefined, has_clip: undefined,
identifier: undefined, recognized_license_plate: undefined,
})); }));
}} }}
> >
@ -847,97 +850,109 @@ export function SnapshotClipFilterContent({
); );
} }
type IdentifierFilterContentProps = { type RecognizedLicensePlatesFilterContentProps = {
identifiers: string[] | undefined; recognizedLicensePlates: string[] | undefined;
setIdentifiers: (identifiers: string[] | undefined) => void; setRecognizedLicensePlates: (
recognizedLicensePlates: string[] | undefined,
) => void;
}; };
export function IdentifierFilterContent({ export function RecognizedLicensePlatesFilterContent({
identifiers, recognizedLicensePlates,
setIdentifiers, setRecognizedLicensePlates,
}: IdentifierFilterContentProps) { }: RecognizedLicensePlatesFilterContentProps) {
const { data: allIdentifiers, error } = useSWR<string[]>("identifiers", { const { data: allRecognizedLicensePlates, error } = useSWR<string[]>(
revalidateOnFocus: false, "recognized_license_plates",
}); {
revalidateOnFocus: false,
const [selectedIdentifiers, setSelectedIdentifiers] = useState<string[]>( },
identifiers || [],
); );
const [selectedRecognizedLicensePlates, setSelectedRecognizedLicensePlates] =
useState<string[]>(recognizedLicensePlates || []);
const [inputValue, setInputValue] = useState(""); const [inputValue, setInputValue] = useState("");
useEffect(() => { useEffect(() => {
if (identifiers) { if (recognizedLicensePlates) {
setSelectedIdentifiers(identifiers); setSelectedRecognizedLicensePlates(recognizedLicensePlates);
} else { } else {
setSelectedIdentifiers([]); setSelectedRecognizedLicensePlates([]);
} }
}, [identifiers]); }, [recognizedLicensePlates]);
const handleSelect = (identifier: string) => { const handleSelect = (recognizedLicensePlate: string) => {
const newSelected = selectedIdentifiers.includes(identifier) const newSelected = selectedRecognizedLicensePlates.includes(
? selectedIdentifiers.filter((id) => id !== identifier) // Deselect recognizedLicensePlate,
: [...selectedIdentifiers, identifier]; // Select )
? selectedRecognizedLicensePlates.filter(
(id) => id !== recognizedLicensePlate,
) // Deselect
: [...selectedRecognizedLicensePlates, recognizedLicensePlate]; // Select
setSelectedIdentifiers(newSelected); setSelectedRecognizedLicensePlates(newSelected);
if (newSelected.length === 0) { if (newSelected.length === 0) {
setIdentifiers(undefined); // Clear filter if no identifiers selected setRecognizedLicensePlates(undefined); // Clear filter if no plates selected
} else { } else {
setIdentifiers(newSelected); setRecognizedLicensePlates(newSelected);
} }
}; };
if (!allIdentifiers || allIdentifiers.length === 0) { if (!allRecognizedLicensePlates || allRecognizedLicensePlates.length === 0) {
return null; return null;
} }
const filteredIdentifiers = const filteredRecognizedLicensePlates =
allIdentifiers?.filter((id) => allRecognizedLicensePlates?.filter((id) =>
id.toLowerCase().includes(inputValue.toLowerCase()), id.toLowerCase().includes(inputValue.toLowerCase()),
) || []; ) || [];
return ( return (
<div className="overflow-x-hidden"> <div className="overflow-x-hidden">
<DropdownMenuSeparator className="mb-3" /> <DropdownMenuSeparator className="mb-3" />
<div className="mb-3 text-lg">Identifiers</div> <div className="mb-3 text-lg">Recognized License Plates</div>
{error ? ( {error ? (
<p className="text-sm text-red-500">Failed to load identifiers</p> <p className="text-sm text-red-500">
) : !allIdentifiers ? ( Failed to load recognized license plates.
<p className="text-sm text-muted-foreground">Loading identifiers...</p> </p>
) : !allRecognizedLicensePlates ? (
<p className="text-sm text-muted-foreground">
Loading recognized license plates...
</p>
) : ( ) : (
<> <>
<Command className="border border-input bg-background"> <Command className="border border-input bg-background">
<CommandInput <CommandInput
placeholder="Type to search identifiers..." placeholder="Type to search license plates..."
value={inputValue} value={inputValue}
onValueChange={setInputValue} onValueChange={setInputValue}
/> />
<CommandList className="max-h-[200px] overflow-auto"> <CommandList className="max-h-[200px] overflow-auto">
{filteredIdentifiers.length === 0 && inputValue && ( {filteredRecognizedLicensePlates.length === 0 && inputValue && (
<CommandEmpty>No identifiers found.</CommandEmpty> <CommandEmpty>No license plates found.</CommandEmpty>
)} )}
{filteredIdentifiers.map((identifier) => ( {filteredRecognizedLicensePlates.map((plate) => (
<CommandItem <CommandItem
key={identifier} key={plate}
value={identifier} value={plate}
onSelect={() => handleSelect(identifier)} onSelect={() => handleSelect(plate)}
className="cursor-pointer" className="cursor-pointer"
> >
<LuCheck <LuCheck
className={cn( className={cn(
"mr-2 h-4 w-4", "mr-2 h-4 w-4",
selectedIdentifiers.includes(identifier) selectedRecognizedLicensePlates.includes(plate)
? "opacity-100" ? "opacity-100"
: "opacity-0", : "opacity-0",
)} )}
/> />
{identifier} {plate}
</CommandItem> </CommandItem>
))} ))}
</CommandList> </CommandList>
</Command> </Command>
{selectedIdentifiers.length > 0 && ( {selectedRecognizedLicensePlates.length > 0 && (
<div className="mt-2 flex flex-wrap gap-2"> <div className="mt-2 flex flex-wrap gap-2">
{selectedIdentifiers.map((id) => ( {selectedRecognizedLicensePlates.map((id) => (
<span <span
key={id} key={id}
className="inline-flex items-center rounded bg-selected px-2 py-1 text-sm text-white" className="inline-flex items-center rounded bg-selected px-2 py-1 text-sm text-white"
@ -956,7 +971,7 @@ export function IdentifierFilterContent({
</> </>
)} )}
<p className="mt-1 text-sm text-muted-foreground"> <p className="mt-1 text-sm text-muted-foreground">
Select one or more identifiers from the list. Select one or more plates from the list.
</p> </p>
</div> </div>
); );

View File

@ -105,7 +105,8 @@ export default function Explore() {
cameras: searchSearchParams["cameras"], cameras: searchSearchParams["cameras"],
labels: searchSearchParams["labels"], labels: searchSearchParams["labels"],
sub_labels: searchSearchParams["sub_labels"], sub_labels: searchSearchParams["sub_labels"],
identifier: searchSearchParams["identifier"], recognized_license_plate:
searchSearchParams["recognized_license_plate"],
zones: searchSearchParams["zones"], zones: searchSearchParams["zones"],
before: searchSearchParams["before"], before: searchSearchParams["before"],
after: searchSearchParams["after"], after: searchSearchParams["after"],
@ -141,7 +142,8 @@ export default function Explore() {
cameras: searchSearchParams["cameras"], cameras: searchSearchParams["cameras"],
labels: searchSearchParams["labels"], labels: searchSearchParams["labels"],
sub_labels: searchSearchParams["sub_labels"], sub_labels: searchSearchParams["sub_labels"],
identifier: searchSearchParams["identifier"], recognized_license_plate:
searchSearchParams["recognized_license_plate"],
zones: searchSearchParams["zones"], zones: searchSearchParams["zones"],
before: searchSearchParams["before"], before: searchSearchParams["before"],
after: searchSearchParams["after"], after: searchSearchParams["after"],

View File

@ -58,8 +58,8 @@ export type SearchResult = {
average_estimated_speed: number; average_estimated_speed: number;
velocity_angle: number; velocity_angle: number;
path_data: [number[], number][]; path_data: [number[], number][];
identifier?: string; recognized_license_plate?: string;
identifier_score?: number; recognized_license_plate_score?: number;
}; };
}; };
@ -68,7 +68,7 @@ export type SearchFilter = {
cameras?: string[]; cameras?: string[];
labels?: string[]; labels?: string[];
sub_labels?: string[]; sub_labels?: string[];
identifier?: string[]; recognized_license_plate?: string[];
zones?: string[]; zones?: string[];
before?: number; before?: number;
after?: number; after?: number;
@ -92,7 +92,7 @@ export type SearchQueryParams = {
cameras?: string[]; cameras?: string[];
labels?: string[]; labels?: string[];
sub_labels?: string[]; sub_labels?: string[];
identifier?: string[]; recognized_license_plate?: string[];
zones?: string[]; zones?: string[];
before?: string; before?: string;
after?: string; after?: string;

View File

@ -121,7 +121,9 @@ export default function SearchView({
}, [config, searchFilter]); }, [config, searchFilter]);
const { data: allSubLabels } = useSWR("sub_labels"); const { data: allSubLabels } = useSWR("sub_labels");
const { data: allIdentifiers } = useSWR("identifiers"); const { data: allRecognizedLicensePlates } = useSWR(
"recognized_license_plates",
);
const allZones = useMemo<string[]>(() => { const allZones = useMemo<string[]>(() => {
if (!config) { if (!config) {
@ -161,13 +163,20 @@ export default function SearchView({
max_score: ["100"], max_score: ["100"],
min_speed: ["1"], min_speed: ["1"],
max_speed: ["150"], max_speed: ["150"],
identifier: allIdentifiers, recognized_license_plate: allRecognizedLicensePlates,
has_clip: ["yes", "no"], has_clip: ["yes", "no"],
has_snapshot: ["yes", "no"], has_snapshot: ["yes", "no"],
...(config?.plus?.enabled && ...(config?.plus?.enabled &&
searchFilter?.has_snapshot && { is_submitted: ["yes", "no"] }), searchFilter?.has_snapshot && { is_submitted: ["yes", "no"] }),
}), }),
[config, allLabels, allZones, allSubLabels, allIdentifiers, searchFilter], [
config,
allLabels,
allZones,
allSubLabels,
allRecognizedLicensePlates,
searchFilter,
],
); );
// remove duplicate event ids // remove duplicate event ids