mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-11-15 10:03:00 +03:00
Use identifier field for unknown license plates (#17123)
* backend * backend fixes * api for search queries * frontend * docs * add filterable scroll list to more filters pane for identifiers * always publish identifier
This commit is contained in:
@@ -619,6 +619,39 @@ def get_sub_labels(split_joined: Optional[int] = None):
|
||||
return JSONResponse(content=sub_labels)
|
||||
|
||||
|
||||
@router.get("/identifiers")
|
||||
def get_identifiers(split_joined: Optional[int] = None):
|
||||
try:
|
||||
events = Event.select(Event.data).distinct()
|
||||
except Exception:
|
||||
return JSONResponse(
|
||||
content=({"success": False, "message": "Failed to get identifiers"}),
|
||||
status_code=404,
|
||||
)
|
||||
|
||||
identifiers = []
|
||||
for e in events:
|
||||
if e.data is not None and "identifier" in e.data:
|
||||
identifiers.append(e.data["identifier"])
|
||||
|
||||
while None in identifiers:
|
||||
identifiers.remove(None)
|
||||
|
||||
if split_joined:
|
||||
original_identifiers = identifiers.copy()
|
||||
for identifier in original_identifiers:
|
||||
if identifier and "," in identifier:
|
||||
identifiers.remove(identifier)
|
||||
parts = identifier.split(",")
|
||||
for part in parts:
|
||||
if part.strip() not in identifiers:
|
||||
identifiers.append(part.strip())
|
||||
|
||||
identifiers = list(set(identifiers))
|
||||
identifiers.sort()
|
||||
return JSONResponse(content=identifiers)
|
||||
|
||||
|
||||
@router.get("/timeline")
|
||||
def timeline(camera: str = "all", limit: int = 100, source_id: Optional[str] = None):
|
||||
clauses = []
|
||||
|
||||
@@ -27,6 +27,7 @@ class EventsQueryParams(BaseModel):
|
||||
max_score: Optional[float] = None
|
||||
min_speed: Optional[float] = None
|
||||
max_speed: Optional[float] = None
|
||||
identifier: Optional[str] = "all"
|
||||
is_submitted: Optional[int] = None
|
||||
min_length: Optional[float] = None
|
||||
max_length: Optional[float] = None
|
||||
@@ -55,6 +56,7 @@ class EventsSearchQueryParams(BaseModel):
|
||||
max_score: Optional[float] = None
|
||||
min_speed: Optional[float] = None
|
||||
max_speed: Optional[float] = None
|
||||
identifier: Optional[str] = "all"
|
||||
sort: Optional[str] = None
|
||||
|
||||
|
||||
|
||||
@@ -101,6 +101,7 @@ def events(params: EventsQueryParams = Depends()):
|
||||
min_length = params.min_length
|
||||
max_length = params.max_length
|
||||
event_id = params.event_id
|
||||
identifier = params.identifier
|
||||
|
||||
sort = params.sort
|
||||
|
||||
@@ -158,6 +159,32 @@ def events(params: EventsQueryParams = Depends()):
|
||||
sub_label_clause = reduce(operator.or_, sub_label_clauses)
|
||||
clauses.append((sub_label_clause))
|
||||
|
||||
if identifier != "all":
|
||||
# use matching so joined identifiers are included
|
||||
# for example an identifier 'ABC123' would get events
|
||||
# with identifiers 'ABC123' and 'ABC123, XYZ789'
|
||||
identifier_clauses = []
|
||||
filtered_identifiers = identifier.split(",")
|
||||
|
||||
if "None" in filtered_identifiers:
|
||||
filtered_identifiers.remove("None")
|
||||
identifier_clauses.append((Event.data["identifier"].is_null()))
|
||||
|
||||
for identifier in filtered_identifiers:
|
||||
# Exact matching plus list inclusion
|
||||
identifier_clauses.append(
|
||||
(Event.data["identifier"].cast("text") == identifier)
|
||||
)
|
||||
identifier_clauses.append(
|
||||
(Event.data["identifier"].cast("text") % f"*{identifier},*")
|
||||
)
|
||||
identifier_clauses.append(
|
||||
(Event.data["identifier"].cast("text") % f"*, {identifier}*")
|
||||
)
|
||||
|
||||
identifier_clause = reduce(operator.or_, identifier_clauses)
|
||||
clauses.append((identifier_clause))
|
||||
|
||||
if zones != "all":
|
||||
# use matching so events with multiple zones
|
||||
# still match on a search where any zone matches
|
||||
@@ -340,6 +367,8 @@ def events_explore(limit: int = 10):
|
||||
"average_estimated_speed",
|
||||
"velocity_angle",
|
||||
"path_data",
|
||||
"identifier",
|
||||
"identifier_score",
|
||||
]
|
||||
},
|
||||
"event_count": label_counts[event.label],
|
||||
@@ -397,6 +426,7 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends())
|
||||
has_clip = params.has_clip
|
||||
has_snapshot = params.has_snapshot
|
||||
is_submitted = params.is_submitted
|
||||
identifier = params.identifier
|
||||
|
||||
# for similarity search
|
||||
event_id = params.event_id
|
||||
@@ -466,6 +496,32 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends())
|
||||
|
||||
event_filters.append((reduce(operator.or_, zone_clauses)))
|
||||
|
||||
if identifier != "all":
|
||||
# use matching so joined identifiers are included
|
||||
# for example an identifier 'ABC123' would get events
|
||||
# with identifiers 'ABC123' and 'ABC123, XYZ789'
|
||||
identifier_clauses = []
|
||||
filtered_identifiers = identifier.split(",")
|
||||
|
||||
if "None" in filtered_identifiers:
|
||||
filtered_identifiers.remove("None")
|
||||
identifier_clauses.append((Event.data["identifier"].is_null()))
|
||||
|
||||
for identifier in filtered_identifiers:
|
||||
# Exact matching plus list inclusion
|
||||
identifier_clauses.append(
|
||||
(Event.data["identifier"].cast("text") == identifier)
|
||||
)
|
||||
identifier_clauses.append(
|
||||
(Event.data["identifier"].cast("text") % f"*{identifier},*")
|
||||
)
|
||||
identifier_clauses.append(
|
||||
(Event.data["identifier"].cast("text") % f"*, {identifier}*")
|
||||
)
|
||||
|
||||
identifier_clause = reduce(operator.or_, identifier_clauses)
|
||||
event_filters.append((identifier_clause))
|
||||
|
||||
if after:
|
||||
event_filters.append((Event.start_time > after))
|
||||
|
||||
@@ -627,6 +683,8 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends())
|
||||
"average_estimated_speed",
|
||||
"velocity_angle",
|
||||
"path_data",
|
||||
"identifier",
|
||||
"identifier_score",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -681,6 +739,7 @@ def events_summary(params: EventsSummaryQueryParams = Depends()):
|
||||
Event.camera,
|
||||
Event.label,
|
||||
Event.sub_label,
|
||||
Event.data,
|
||||
fn.strftime(
|
||||
"%Y-%m-%d",
|
||||
fn.datetime(
|
||||
@@ -695,6 +754,7 @@ def events_summary(params: EventsSummaryQueryParams = Depends()):
|
||||
Event.camera,
|
||||
Event.label,
|
||||
Event.sub_label,
|
||||
Event.data,
|
||||
(Event.start_time + seconds_offset).cast("int") / (3600 * 24),
|
||||
Event.zones,
|
||||
)
|
||||
|
||||
@@ -137,12 +137,13 @@ class CameraState:
|
||||
# draw the bounding boxes on the frame
|
||||
box = obj["box"]
|
||||
text = (
|
||||
obj["label"]
|
||||
obj["sub_label"][0]
|
||||
if (
|
||||
not obj.get("sub_label")
|
||||
or not is_label_printable(obj["sub_label"][0])
|
||||
obj.get("sub_label") and is_label_printable(obj["sub_label"][0])
|
||||
)
|
||||
else obj["sub_label"][0]
|
||||
else obj.get("identifier", [None])[0]
|
||||
if (obj.get("identifier") and obj["identifier"][0])
|
||||
else obj["label"]
|
||||
)
|
||||
draw_box_with_label(
|
||||
frame_copy,
|
||||
|
||||
@@ -14,6 +14,7 @@ class EventMetadataTypeEnum(str, Enum):
|
||||
manual_event_end = "manual_event_end"
|
||||
regenerate_description = "regenerate_description"
|
||||
sub_label = "sub_label"
|
||||
identifier = "identifier"
|
||||
|
||||
|
||||
class EventMetadataPublisher(Publisher):
|
||||
|
||||
@@ -1054,13 +1054,19 @@ class LicensePlateProcessingMixin:
|
||||
for plate in plates
|
||||
)
|
||||
),
|
||||
top_plate,
|
||||
None,
|
||||
)
|
||||
|
||||
# Send the result to the API
|
||||
# If it's a known plate, publish to sub_label
|
||||
if sub_label is not None:
|
||||
self.sub_label_publisher.publish(
|
||||
EventMetadataTypeEnum.sub_label, (id, sub_label, avg_confidence)
|
||||
)
|
||||
|
||||
self.sub_label_publisher.publish(
|
||||
EventMetadataTypeEnum.sub_label, (id, sub_label, avg_confidence)
|
||||
EventMetadataTypeEnum.identifier, (id, top_plate, avg_confidence)
|
||||
)
|
||||
|
||||
self.detected_license_plates[id] = {
|
||||
"plate": top_plate,
|
||||
"char_confidences": top_char_confidences,
|
||||
|
||||
@@ -27,6 +27,7 @@ def should_update_db(prev_event: Event, current_event: Event) -> bool:
|
||||
or prev_event["average_estimated_speed"]
|
||||
!= current_event["average_estimated_speed"]
|
||||
or prev_event["velocity_angle"] != current_event["velocity_angle"]
|
||||
or prev_event["identifier"] != current_event["identifier"]
|
||||
or prev_event["path_data"] != current_event["path_data"]
|
||||
):
|
||||
return True
|
||||
@@ -226,6 +227,11 @@ class EventProcessor(threading.Thread):
|
||||
event[Event.sub_label] = event_data["sub_label"][0]
|
||||
event[Event.data]["sub_label_score"] = event_data["sub_label"][1]
|
||||
|
||||
# only overwrite the identifier in the database if it's set
|
||||
if event_data.get("identifier") is not None:
|
||||
event[Event.data]["identifier"] = event_data["identifier"][0]
|
||||
event[Event.data]["identifier_score"] = event_data["identifier"][1]
|
||||
|
||||
(
|
||||
Event.insert(event)
|
||||
.on_conflict(
|
||||
|
||||
@@ -346,6 +346,41 @@ class TrackedObjectProcessor(threading.Thread):
|
||||
|
||||
return True
|
||||
|
||||
def set_identifier(
|
||||
self, event_id: str, identifier: str | None, score: float | None
|
||||
) -> None:
|
||||
"""Update identifier for given event id."""
|
||||
tracked_obj: TrackedObject = None
|
||||
|
||||
for state in self.camera_states.values():
|
||||
tracked_obj = state.tracked_objects.get(event_id)
|
||||
|
||||
if tracked_obj is not None:
|
||||
break
|
||||
|
||||
try:
|
||||
event: Event = Event.get(Event.id == event_id)
|
||||
except DoesNotExist:
|
||||
event = None
|
||||
|
||||
if not tracked_obj and not event:
|
||||
return
|
||||
|
||||
if tracked_obj:
|
||||
tracked_obj.obj_data["identifier"] = (identifier, score)
|
||||
|
||||
if event:
|
||||
data = event.data
|
||||
data["identifier"] = identifier
|
||||
if identifier is None:
|
||||
data["identifier_score"] = None
|
||||
elif score is not None:
|
||||
data["identifier_score"] = score
|
||||
event.data = data
|
||||
event.save()
|
||||
|
||||
return True
|
||||
|
||||
def create_manual_event(self, payload: tuple) -> None:
|
||||
(
|
||||
frame_time,
|
||||
@@ -507,6 +542,9 @@ class TrackedObjectProcessor(threading.Thread):
|
||||
if topic.endswith(EventMetadataTypeEnum.sub_label.value):
|
||||
(event_id, sub_label, score) = payload
|
||||
self.set_sub_label(event_id, sub_label, score)
|
||||
if topic.endswith(EventMetadataTypeEnum.identifier.value):
|
||||
(event_id, identifier, score) = payload
|
||||
self.set_identifier(event_id, identifier, score)
|
||||
elif topic.endswith(EventMetadataTypeEnum.manual_event_create.value):
|
||||
self.create_manual_event(payload)
|
||||
elif topic.endswith(EventMetadataTypeEnum.manual_event_end.value):
|
||||
|
||||
@@ -153,6 +153,8 @@ class TrackedObject:
|
||||
"current_estimated_speed": self.current_estimated_speed,
|
||||
"velocity_angle": self.velocity_angle,
|
||||
"path_data": self.path_data,
|
||||
"identifier": obj_data.get("identifier"),
|
||||
"identifier_score": obj_data.get("identifier_score"),
|
||||
}
|
||||
thumb_update = True
|
||||
|
||||
@@ -365,6 +367,7 @@ class TrackedObject:
|
||||
"average_estimated_speed": self.average_estimated_speed,
|
||||
"velocity_angle": self.velocity_angle,
|
||||
"path_data": self.path_data,
|
||||
"identifier": self.obj_data.get("identifier"),
|
||||
}
|
||||
|
||||
return event
|
||||
|
||||
Reference in New Issue
Block a user