enhancement and debugging config

This commit is contained in:
Josh Hawkins 2025-03-28 07:05:46 -05:00
parent f6172cb1e4
commit 9e34581faf
2 changed files with 71 additions and 16 deletions

View File

@ -126,6 +126,16 @@ class LicensePlateRecognitionConfig(FrigateBaseModel):
known_plates: Optional[Dict[str, List[str]]] = Field( known_plates: Optional[Dict[str, List[str]]] = Field(
default={}, title="Known plates to track (strings or regular expressions)." default={}, title="Known plates to track (strings or regular expressions)."
) )
enhancement: int = Field(
default=0,
title="Amount of contrast adjustment and denoising to apply to license plate images before recognition.",
ge=0,
le=10,
)
debug_save_plates: bool = Field(
default=False,
title="Save plates captured for LPR for debugging purposes.",
)
class CameraLicensePlateRecognitionConfig(FrigateBaseModel): class CameraLicensePlateRecognitionConfig(FrigateBaseModel):
@ -139,5 +149,11 @@ class CameraLicensePlateRecognitionConfig(FrigateBaseModel):
default=1000, default=1000,
title="Minimum area of license plate to begin running recognition.", title="Minimum area of license plate to begin running recognition.",
) )
enhancement: int = Field(
default=0,
title="Amount of contrast adjustment and denoising to apply to license plate images before recognition.",
ge=0,
le=10,
)
model_config = ConfigDict(extra="ignore", protected_namespaces=()) model_config = ConfigDict(extra="ignore", protected_namespaces=())

View File

@ -4,9 +4,11 @@ import base64
import datetime import datetime
import logging import logging
import math import math
import os
import random import random
import re import re
import string import string
from pathlib import Path
from typing import List, Optional, Tuple from typing import List, Optional, Tuple
import cv2 import cv2
@ -20,6 +22,7 @@ from frigate.comms.event_metadata_updater import (
EventMetadataTypeEnum, EventMetadataTypeEnum,
) )
from frigate.config.camera.camera import CameraTypeEnum from frigate.config.camera.camera import CameraTypeEnum
from frigate.const import CLIPS_DIR
from frigate.embeddings.onnx.lpr_embedding import LPR_EMBEDDING_SIZE from frigate.embeddings.onnx.lpr_embedding import LPR_EMBEDDING_SIZE
from frigate.util.image import area from frigate.util.image import area
@ -107,7 +110,7 @@ class LicensePlateProcessingMixin:
return self._process_classification_output(images, outputs) return self._process_classification_output(images, outputs)
def _recognize( def _recognize(
self, images: List[np.ndarray] self, camera: string, images: List[np.ndarray]
) -> Tuple[List[str], List[List[float]]]: ) -> Tuple[List[str], List[List[float]]]:
""" """
Recognize the characters on the detected license plates using the recognition model. Recognize the characters on the detected license plates using the recognition model.
@ -137,7 +140,7 @@ class LicensePlateProcessingMixin:
# preprocess the images based on the max aspect ratio # preprocess the images based on the max aspect ratio
for i in range(index, min(num_images, index + self.batch_size)): for i in range(index, min(num_images, index + self.batch_size)):
norm_image = self._preprocess_recognition_image( norm_image = self._preprocess_recognition_image(
images[indices[i]], max_wh_ratio camera, images[indices[i]], max_wh_ratio
) )
norm_image = norm_image[np.newaxis, :] norm_image = norm_image[np.newaxis, :]
norm_images.append(norm_image) norm_images.append(norm_image)
@ -146,7 +149,7 @@ class LicensePlateProcessingMixin:
return self.ctc_decoder(outputs) return self.ctc_decoder(outputs)
def _process_license_plate( def _process_license_plate(
self, image: np.ndarray self, camera: string, id: string, image: np.ndarray
) -> Tuple[List[str], List[float], List[int]]: ) -> Tuple[List[str], List[float], List[int]]:
""" """
Complete pipeline for detecting, classifying, and recognizing license plates in the input image. Complete pipeline for detecting, classifying, and recognizing license plates in the input image.
@ -174,21 +177,37 @@ class LicensePlateProcessingMixin:
boxes = self._sort_boxes(list(boxes)) boxes = self._sort_boxes(list(boxes))
plate_images = [self._crop_license_plate(image, x) for x in boxes] plate_images = [self._crop_license_plate(image, x) for x in boxes]
if WRITE_DEBUG_IMAGES:
current_time = int(datetime.datetime.now().timestamp()) current_time = int(datetime.datetime.now().timestamp())
if WRITE_DEBUG_IMAGES:
for i, img in enumerate(plate_images): for i, img in enumerate(plate_images):
cv2.imwrite( cv2.imwrite(
f"debug/frames/license_plate_cropped_{current_time}_{i + 1}.jpg", f"debug/frames/license_plate_cropped_{current_time}_{i + 1}.jpg",
img, img,
) )
if self.config.lpr.debug_save_plates:
logger.debug(f"{camera}: Saving plates for event {id}")
Path(os.path.join(CLIPS_DIR, f"lpr/{camera}/{id}")).mkdir(
parents=True, exist_ok=True
)
for i, img in enumerate(plate_images):
cv2.imwrite(
os.path.join(
CLIPS_DIR, f"lpr/{camera}/{id}/{current_time}_{i + 1}.jpg"
),
img,
)
# keep track of the index of each image for correct area calc later # keep track of the index of each image for correct area calc later
sorted_indices = np.argsort([x.shape[1] / x.shape[0] for x in plate_images]) sorted_indices = np.argsort([x.shape[1] / x.shape[0] for x in plate_images])
reverse_mapping = { reverse_mapping = {
idx: original_idx for original_idx, idx in enumerate(sorted_indices) idx: original_idx for original_idx, idx in enumerate(sorted_indices)
} }
results, confidences = self._recognize(plate_images) results, confidences = self._recognize(camera, plate_images)
if results: if results:
license_plates = [""] * len(plate_images) license_plates = [""] * len(plate_images)
@ -606,7 +625,7 @@ class LicensePlateProcessingMixin:
return images, results return images, results
def _preprocess_recognition_image( def _preprocess_recognition_image(
self, image: np.ndarray, max_wh_ratio: float self, camera: string, image: np.ndarray, max_wh_ratio: float
) -> np.ndarray: ) -> np.ndarray:
""" """
Preprocess an image for recognition by dynamically adjusting its width. Preprocess an image for recognition by dynamically adjusting its width.
@ -634,20 +653,38 @@ class LicensePlateProcessingMixin:
else: else:
gray = image gray = image
if False: if self.config.cameras[camera].lpr.enhancement > 3:
smoothed = cv2.bilateralFilter(gray, d=3, sigmaColor=50, sigmaSpace=50) # denoise using a configurable pixel neighborhood value
logger.debug(
f"{camera}: Denoising recognition image (level: {self.config.cameras[camera].lpr.enhancement})"
)
smoothed = cv2.bilateralFilter(
gray,
d=5 + self.config.cameras[camera].lpr.enhancement,
sigmaColor=10 * self.config.cameras[camera].lpr.enhancement,
sigmaSpace=10 * self.config.cameras[camera].lpr.enhancement,
)
sharpening_kernel = np.array([[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]]) sharpening_kernel = np.array([[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]])
processed = cv2.filter2D(smoothed, -1, sharpening_kernel) processed = cv2.filter2D(smoothed, -1, sharpening_kernel)
else: else:
processed = gray processed = gray
# apply CLAHE for contrast enhancement if self.config.cameras[camera].lpr.enhancement > 0:
# always apply the same CLAHE for contrast enhancement when enhancement level is above 3
logger.debug(
f"{camera}: Enhancing contrast for recognition image (level: {self.config.cameras[camera].lpr.enhancement})"
)
grid_size = ( grid_size = (
max(4, input_w // 40), max(4, input_w // 40),
max(4, input_h // 40), max(4, input_h // 40),
) )
clahe = cv2.createCLAHE(clipLimit=1.5, tileGridSize=grid_size) clahe = cv2.createCLAHE(
clipLimit=2 if self.config.cameras[camera].lpr.enhancement > 5 else 1.5,
tileGridSize=grid_size,
)
enhanced = clahe.apply(processed) enhanced = clahe.apply(processed)
else:
enhanced = processed
# Convert back to 3-channel for model compatibility # Convert back to 3-channel for model compatibility
image = cv2.cvtColor(enhanced, cv2.COLOR_GRAY2RGB) image = cv2.cvtColor(enhanced, cv2.COLOR_GRAY2RGB)
@ -955,6 +992,8 @@ class LicensePlateProcessingMixin:
return return
if dedicated_lpr: if dedicated_lpr:
id = "dedicated-lpr"
rgb = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420) rgb = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420)
# apply motion mask # apply motion mask
@ -1156,7 +1195,7 @@ class LicensePlateProcessingMixin:
# run detection, returns results sorted by confidence, best first # run detection, returns results sorted by confidence, best first
start = datetime.datetime.now().timestamp() start = datetime.datetime.now().timestamp()
license_plates, confidences, areas = self._process_license_plate( license_plates, confidences, areas = self._process_license_plate(
license_plate_frame camera, id, license_plate_frame
) )
self.__update_lpr_metrics(datetime.datetime.now().timestamp() - start) self.__update_lpr_metrics(datetime.datetime.now().timestamp() - start)