LPR bugfix (#17384)

* ensure image is numpy array

* clean up debugging

* clean up postprocessor

* process raw input as img
This commit is contained in:
Josh Hawkins 2025-03-26 07:41:00 -05:00 committed by GitHub
parent 4ccf61a6d7
commit 4edf0d8cd3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 30 additions and 30 deletions

View File

@ -32,10 +32,6 @@ class LicensePlateProcessingMixin:
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.requires_license_plate_detection = (
"license_plate" not in self.config.objects.all_objects
)
self.event_metadata_publisher = EventMetadataPublisher() self.event_metadata_publisher = EventMetadataPublisher()
self.ctc_decoder = CTCDecoder() self.ctc_decoder = CTCDecoder()
@ -312,7 +308,6 @@ class LicensePlateProcessingMixin:
# get minimum bounding box (rotated rectangle) around the contour and the smallest side length. # get minimum bounding box (rotated rectangle) around the contour and the smallest side length.
points, min_side = self._get_min_boxes(contour) points, min_side = self._get_min_boxes(contour)
logger.debug(f"min side {index}, {min_side}")
if min_side < self.min_size: if min_side < self.min_size:
continue continue
@ -320,7 +315,6 @@ class LicensePlateProcessingMixin:
points = np.array(points) points = np.array(points)
score = self._box_score(output, contour) score = self._box_score(output, contour)
logger.debug(f"box score {index}, {score}")
if self.box_thresh > score: if self.box_thresh > score:
continue continue
@ -991,21 +985,21 @@ class LicensePlateProcessingMixin:
license_plate = self._detect_license_plate(rgb) license_plate = self._detect_license_plate(rgb)
logger.debug( logger.debug(
f"YOLOv9 LPD inference time: {(datetime.datetime.now().timestamp() - yolov9_start) * 1000:.2f} ms" f"{camera}: YOLOv9 LPD inference time: {(datetime.datetime.now().timestamp() - yolov9_start) * 1000:.2f} ms"
) )
self.__update_yolov9_metrics( self.__update_yolov9_metrics(
datetime.datetime.now().timestamp() - yolov9_start datetime.datetime.now().timestamp() - yolov9_start
) )
if not license_plate: if not license_plate:
logger.debug("Detected no license plates in full frame.") logger.debug(f"{camera}: Detected no license plates in full frame.")
return return
license_plate_area = (license_plate[2] - license_plate[0]) * ( license_plate_area = (license_plate[2] - license_plate[0]) * (
license_plate[3] - license_plate[1] license_plate[3] - license_plate[1]
) )
if license_plate_area < self.lpr_config.min_area: if license_plate_area < self.lpr_config.min_area:
logger.debug("License plate area below minimum threshold.") logger.debug(f"{camera}: License plate area below minimum threshold.")
return return
license_plate_frame = rgb[ license_plate_frame = rgb[
@ -1027,13 +1021,15 @@ class LicensePlateProcessingMixin:
# don't run for non car objects # don't run for non car objects
if obj_data.get("label") != "car": if obj_data.get("label") != "car":
logger.debug("Not a processing license plate for non car object.") logger.debug(
f"{camera}: Not a processing license plate for non car object."
)
return return
# don't run for stationary car objects # don't run for stationary car objects
if obj_data.get("stationary") == True: if obj_data.get("stationary") == True:
logger.debug( logger.debug(
"Not a processing license plate for a stationary car object." f"{camera}: Not a processing license plate for a stationary car object."
) )
return return
@ -1041,14 +1037,14 @@ class LicensePlateProcessingMixin:
# that is not a license plate # that is not a license plate
if obj_data.get("sub_label") and id not in self.detected_license_plates: if obj_data.get("sub_label") and id not in self.detected_license_plates:
logger.debug( logger.debug(
f"Not processing license plate due to existing sub label: {obj_data.get('sub_label')}." f"{camera}: Not processing license plate due to existing sub label: {obj_data.get('sub_label')}."
) )
return return
license_plate: Optional[dict[str, any]] = None license_plate: Optional[dict[str, any]] = None
if self.requires_license_plate_detection: if "license_plate" not in self.config.cameras[camera].objects.track:
logger.debug("Running manual license_plate detection.") logger.debug(f"{camera}: Running manual license_plate detection.")
car_box = obj_data.get("box") car_box = obj_data.get("box")
@ -1071,14 +1067,16 @@ class LicensePlateProcessingMixin:
yolov9_start = datetime.datetime.now().timestamp() yolov9_start = datetime.datetime.now().timestamp()
license_plate = self._detect_license_plate(car) license_plate = self._detect_license_plate(car)
logger.debug( logger.debug(
f"YOLOv9 LPD inference time: {(datetime.datetime.now().timestamp() - yolov9_start) * 1000:.2f} ms" f"{camera}: YOLOv9 LPD inference time: {(datetime.datetime.now().timestamp() - yolov9_start) * 1000:.2f} ms"
) )
self.__update_yolov9_metrics( self.__update_yolov9_metrics(
datetime.datetime.now().timestamp() - yolov9_start datetime.datetime.now().timestamp() - yolov9_start
) )
if not license_plate: if not license_plate:
logger.debug("Detected no license plates for car object.") logger.debug(
f"{camera}: Detected no license plates for car object."
)
return return
license_plate_area = max( license_plate_area = max(
@ -1093,7 +1091,7 @@ class LicensePlateProcessingMixin:
license_plate_area license_plate_area
< self.config.cameras[obj_data["camera"]].lpr.min_area * 2 < self.config.cameras[obj_data["camera"]].lpr.min_area * 2
): ):
logger.debug("License plate is less than min_area") logger.debug(f"{camera}: License plate is less than min_area")
return return
license_plate_frame = car[ license_plate_frame = car[
@ -1103,7 +1101,7 @@ class LicensePlateProcessingMixin:
else: else:
# don't run for object without attributes # don't run for object without attributes
if not obj_data.get("current_attributes"): if not obj_data.get("current_attributes"):
logger.debug("No attributes to parse.") logger.debug(f"{camera}: No attributes to parse.")
return return
attributes: list[dict[str, any]] = obj_data.get( attributes: list[dict[str, any]] = obj_data.get(
@ -1130,7 +1128,7 @@ class LicensePlateProcessingMixin:
or area(license_plate_box) or area(license_plate_box)
< self.config.cameras[obj_data["camera"]].lpr.min_area < self.config.cameras[obj_data["camera"]].lpr.min_area
): ):
logger.debug(f"Invalid license plate box {license_plate}") logger.debug(f"{camera}: Invalid license plate box {license_plate}")
return return
license_plate_frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420) license_plate_frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420)
@ -1184,7 +1182,7 @@ class LicensePlateProcessingMixin:
) )
logger.debug( logger.debug(
f"Detected text: {plate} (average confidence: {avg_confidence:.2f}, area: {text_area} pixels)" f"{camera}: Detected text: {plate} (average confidence: {avg_confidence:.2f}, area: {text_area} pixels)"
) )
else: else:
logger.debug("No text detected") logger.debug("No text detected")
@ -1204,7 +1202,7 @@ class LicensePlateProcessingMixin:
# Check against minimum confidence threshold # Check against minimum confidence threshold
if avg_confidence < self.lpr_config.recognition_threshold: if avg_confidence < self.lpr_config.recognition_threshold:
logger.debug( logger.debug(
f"Average confidence {avg_confidence} is less than threshold ({self.lpr_config.recognition_threshold})" f"{camera}: Average confidence {avg_confidence} is less than threshold ({self.lpr_config.recognition_threshold})"
) )
return return
@ -1223,7 +1221,7 @@ class LicensePlateProcessingMixin:
if similarity >= self.similarity_threshold: if similarity >= self.similarity_threshold:
plate_id = existing_id plate_id = existing_id
logger.debug( logger.debug(
f"Matched plate {top_plate} to {data['plate']} (similarity: {similarity:.3f})" f"{camera}: Matched plate {top_plate} to {data['plate']} (similarity: {similarity:.3f})"
) )
break break
if plate_id is None: if plate_id is None:
@ -1231,11 +1229,11 @@ class LicensePlateProcessingMixin:
obj_data, top_plate, avg_confidence obj_data, top_plate, avg_confidence
) )
logger.debug( logger.debug(
f"New plate event for dedicated LPR camera {plate_id}: {top_plate}" f"{camera}: New plate event for dedicated LPR camera {plate_id}: {top_plate}"
) )
else: else:
logger.debug( logger.debug(
f"Matched existing plate event for dedicated LPR camera {plate_id}: {top_plate}" f"{camera}: Matched existing plate event for dedicated LPR camera {plate_id}: {top_plate}"
) )
self.detected_license_plates[plate_id]["last_seen"] = current_time self.detected_license_plates[plate_id]["last_seen"] = current_time
@ -1246,7 +1244,7 @@ class LicensePlateProcessingMixin:
if self._should_keep_previous_plate( if self._should_keep_previous_plate(
id, top_plate, top_char_confidences, top_area, avg_confidence id, top_plate, top_char_confidences, top_area, avg_confidence
): ):
logger.debug("Keeping previous plate") logger.debug(f"{camera}: Keeping previous plate")
return return
# Determine subLabel based on known plates, use regex matching # Determine subLabel based on known plates, use regex matching
@ -1277,7 +1275,9 @@ class LicensePlateProcessingMixin:
if dedicated_lpr: if dedicated_lpr:
# save the best snapshot # save the best snapshot
logger.debug(f"Writing snapshot for {id}, {top_plate}, {current_time}") logger.debug(
f"{camera}: Writing snapshot for {id}, {top_plate}, {current_time}"
)
frame_bgr = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420) frame_bgr = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420)
self.sub_label_publisher.publish( self.sub_label_publisher.publish(
EventMetadataTypeEnum.save_lpr_snapshot, EventMetadataTypeEnum.save_lpr_snapshot,

View File

@ -139,7 +139,7 @@ class LicensePlatePostProcessor(LicensePlateProcessingMixin, PostProcessorApi):
scale_y = image.shape[0] / detect_height scale_y = image.shape[0] / detect_height
# Determine which box to enlarge based on detection mode # Determine which box to enlarge based on detection mode
if self.requires_license_plate_detection: if "license_plate" not in self.config.cameras[camera_name].objects.track:
# Scale and enlarge the car box # Scale and enlarge the car box
box = obj_data.get("box") box = obj_data.get("box")
if not box: if not box:
@ -189,7 +189,7 @@ class LicensePlatePostProcessor(LicensePlateProcessingMixin, PostProcessorApi):
) )
keyframe_obj_data = obj_data.copy() keyframe_obj_data = obj_data.copy()
if self.requires_license_plate_detection: if "license_plate" not in self.config.cameras[camera_name].objects.track:
# car box # car box
keyframe_obj_data["box"] = [new_left, new_top, new_right, new_bottom] keyframe_obj_data["box"] = [new_left, new_top, new_right, new_bottom]
else: else:

View File

@ -261,8 +261,8 @@ class LicensePlateDetector(BaseEmbedding):
def _preprocess_inputs(self, raw_inputs): def _preprocess_inputs(self, raw_inputs):
if isinstance(raw_inputs, list): if isinstance(raw_inputs, list):
raise ValueError("License plate embedding does not support batch inputs.") raise ValueError("License plate embedding does not support batch inputs.")
# Get image as numpy array
img = self._process_image(raw_inputs) img = raw_inputs
height, width, channels = img.shape height, width, channels = img.shape
# Resize maintaining aspect ratio # Resize maintaining aspect ratio