My-LPR-Demo / app.py
Rhaya03's picture
Update app.py
504d68e verified
# --- 1. Import all the necessary tools ---
import gradio as gr
from ultralytics import YOLO
from huggingface_hub import hf_hub_download
import numpy as np
import cv2
import roboflow
from collections import Counter, defaultdict
import re
# --- 2. Load BOTH of your AI models ---
print("Downloading and loading models...")
# --- Model 1: The Character Detector (from Hugging Face) ---
character_model_path = hf_hub_download(
repo_id="MKgoud/License-Plate-Character-Detector",
filename="Charcter-LP.pt"
)
character_model = YOLO(character_model_path)
print("βœ… Character Detector loaded.")
# --- Model 2: The Plate Detector (from Roboflow) ---
ROBOFLOW_API_KEY = "YfKCsreNkoXYFD1CfMBY"
DETECTOR_WORKSPACE_ID = "mylprproject"
DETECTOR_PROJECT_ID = "license-plate-yuw1z-kirke"
DETECTOR_VERSION_NUMBER = 1
rf = roboflow.Roboflow(api_key=ROBOFLOW_API_KEY)
project_detector = rf.workspace(DETECTOR_WORKSPACE_ID).project(DETECTOR_PROJECT_ID)
plate_model = project_detector.version(DETECTOR_VERSION_NUMBER).model
print("βœ… Plate Detector loaded.")
# --- 3. Enhanced preprocessing functions ---
def enhance_plate_image(plate_crop):
"""
Apply multiple enhancement techniques to improve character visibility
"""
enhanced_crops = []
# Original image
enhanced_crops.append(plate_crop)
# Convert to grayscale and back to RGB for consistent processing
gray = cv2.cvtColor(plate_crop, cv2.COLOR_RGB2GRAY)
# Enhancement 1: Adaptive histogram equalization
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
enhanced_gray = clahe.apply(gray)
enhanced_crops.append(cv2.cvtColor(enhanced_gray, cv2.COLOR_GRAY2RGB))
# Enhancement 2: Gaussian blur + unsharp mask
blurred = cv2.GaussianBlur(gray, (3, 3), 0)
unsharp = cv2.addWeighted(gray, 1.8, blurred, -0.8, 0)
unsharp = np.clip(unsharp, 0, 255).astype(np.uint8)
enhanced_crops.append(cv2.cvtColor(unsharp, cv2.COLOR_GRAY2RGB))
# Enhancement 3: Contrast stretching
min_val, max_val = np.percentile(gray, [2, 98])
stretched = np.clip((gray - min_val) * 255 / (max_val - min_val), 0, 255).astype(np.uint8)
enhanced_crops.append(cv2.cvtColor(stretched, cv2.COLOR_GRAY2RGB))
# Enhancement 4: Gamma correction
gamma_corrected = np.power(gray / 255.0, 0.7) * 255
gamma_corrected = gamma_corrected.astype(np.uint8)
enhanced_crops.append(cv2.cvtColor(gamma_corrected, cv2.COLOR_GRAY2RGB))
return enhanced_crops
def smart_character_correction(text, pattern_analysis=True):
"""
Intelligent character correction based on license plate patterns
"""
if not text or len(text) < 3:
return text
# Remove any spaces first
text = text.replace(" ", "").upper()
# Philippine license plate patterns analysis
def analyze_likely_pattern(s):
"""Determine if sequence should be letters or numbers based on position and context"""
if len(s) < 6:
return s
# Common Philippine patterns: ABC123, ABC1234, 123ABC
# Most common is 3 letters + 3-4 numbers
corrected = list(s)
# Pattern 1: First 3 characters are typically letters
for i in range(min(3, len(corrected))):
char = corrected[i]
if char.isdigit():
# Convert numbers that look like letters
digit_to_letter = {'0': 'O', '1': 'I', '2': 'Z', '5': 'S', '6': 'G', '8': 'B'}
if char in digit_to_letter:
corrected[i] = digit_to_letter[char]
# Pattern 2: Characters after position 3 are typically numbers
for i in range(3, len(corrected)):
char = corrected[i]
if char.isalpha():
# Convert letters that look like numbers
letter_to_digit = {'O': '0', 'I': '1', 'L': '1', 'S': '5', 'G': '6', 'B': '8', 'Z': '2', 'T': '7'}
if char in letter_to_digit:
corrected[i] = letter_to_digit[char]
return ''.join(corrected)
# Apply pattern-based corrections if enabled
if pattern_analysis and len(text) >= 6:
text = analyze_likely_pattern(text)
# Additional common OCR error corrections
ocr_corrections = {
# Numbers that might be misread as letters
'Q': '0', # Q often confused with O/0
'D': '0', # D sometimes looks like 0
# Letters that might be misread as numbers
'A': 'A', # Keep A as is (could be confused with 4 but A is common in plates)
}
# Apply only high-confidence corrections
for old_char, new_char in ocr_corrections.items():
text = text.replace(old_char, new_char)
return text
def advanced_detection_filtering(boxes, character_results, plate_crop_shape, min_confidence=0.25):
"""
Advanced filtering with clustering and statistical analysis
"""
if len(boxes) == 0:
return []
detections = []
# Extract all detection info
for box in boxes:
confidence = float(box.conf[0])
if confidence < min_confidence:
continue
class_id = int(box.cls[0])
character = character_results[0].names[class_id]
x1, y1, x2, y2 = box.xyxy[0]
detections.append({
'char': character,
'conf': confidence,
'x1': float(x1), 'y1': float(y1), 'x2': float(x2), 'y2': float(y2),
'width': float(x2 - x1),
'height': float(y2 - y1),
'center_x': float((x1 + x2) / 2),
'center_y': float((y1 + y2) / 2),
'area': float((x2 - x1) * (y2 - y1))
})
if len(detections) == 0:
return []
plate_height, plate_width = plate_crop_shape[:2]
# Step 1: Focus on main character area (upper 75% of plate)
main_area_threshold = plate_height * 0.75
main_detections = [d for d in detections if d['center_y'] <= main_area_threshold]
if len(main_detections) < 3: # If too few in main area, expand slightly
main_area_threshold = plate_height * 0.85
main_detections = [d for d in detections if d['center_y'] <= main_area_threshold]
if len(main_detections) == 0:
main_detections = detections
# Step 2: Statistical filtering based on size and position
heights = [d['height'] for d in main_detections]
widths = [d['width'] for d in main_detections]
y_positions = [d['center_y'] for d in main_detections]
areas = [d['area'] for d in main_detections]
# Calculate robust statistics (using percentiles to avoid outlier influence)
median_height = np.median(heights)
median_width = np.median(widths)
median_y = np.median(y_positions)
q75_area = np.percentile(areas, 75)
# Step 3: Multi-criteria filtering
filtered_detections = []
for detection in main_detections:
# Size consistency check
height_ratio = detection['height'] / median_height
width_ratio = detection['width'] / median_width
# Vertical alignment check
y_deviation = abs(detection['center_y'] - median_y)
max_y_deviation = median_height * 0.5
# Minimum size threshold (avoid tiny noise detections)
min_size_threshold = plate_height * 0.12
# Area-based filtering (avoid unusually small detections)
area_threshold = q75_area * 0.3
# Apply all criteria
if (0.4 <= height_ratio <= 2.5 and
0.3 <= width_ratio <= 3.0 and
y_deviation <= max_y_deviation and
detection['height'] >= min_size_threshold and
detection['area'] >= area_threshold):
filtered_detections.append(detection)
# Step 4: Remove duplicate detections (same character in nearby positions)
final_detections = []
used_positions = []
# Sort by confidence first
filtered_detections.sort(key=lambda x: x['conf'], reverse=True)
for detection in filtered_detections:
# Check if this position is too close to already used positions
too_close = False
for used_x in used_positions:
if abs(detection['center_x'] - used_x) < median_width * 0.8:
too_close = True
break
if not too_close:
final_detections.append(detection)
used_positions.append(detection['center_x'])
return final_detections
def ensemble_character_voting(all_detections, plate_width, confidence_threshold=0.35):
"""
Advanced ensemble voting with spatial clustering and confidence weighting
"""
if not all_detections:
return []
# Step 1: Spatial clustering - group detections by x-position
position_groups = defaultdict(list)
cluster_tolerance = plate_width * 0.15 # 15% of plate width
for detection in all_detections:
x_pos = detection['center_x']
# Find existing cluster or create new one
assigned = False
for cluster_center in list(position_groups.keys()):
if abs(x_pos - cluster_center) <= cluster_tolerance:
position_groups[cluster_center].append(detection)
assigned = True
break
if not assigned:
position_groups[x_pos].append(detection)
# Step 2: For each spatial cluster, determine best character
final_characters = []
for cluster_center, cluster_detections in position_groups.items():
# Group by character within cluster
char_groups = defaultdict(list)
for det in cluster_detections:
char_groups[det['char']].append(det)
# Calculate weighted score for each character
best_char = None
best_score = 0
best_detection = None
for char, char_detections in char_groups.items():
# Calculate score: weighted average of confidence + occurrence bonus
confidences = [d['conf'] for d in char_detections]
avg_confidence = np.mean(confidences)
max_confidence = max(confidences)
occurrence_bonus = min(len(char_detections) * 0.1, 0.3) # Up to 30% bonus
# Final score combines average confidence, max confidence, and occurrence
score = (avg_confidence * 0.5 + max_confidence * 0.4 + occurrence_bonus * 0.1)
if score > best_score and avg_confidence > confidence_threshold:
best_score = score
best_char = char
# Use the detection with highest confidence as representative
best_detection = max(char_detections, key=lambda x: x['conf'])
if best_char and best_detection:
best_detection['final_char'] = best_char
best_detection['final_score'] = best_score
best_detection['cluster_size'] = len(cluster_detections)
final_characters.append(best_detection)
# Step 3: Sort by x-position for final ordering
final_characters.sort(key=lambda x: x['center_x'])
return final_characters
# --- 4. Enhanced main prediction function ---
def detect_license_plate(input_image):
"""
Enhanced version with improved filtering and ensemble voting
"""
print("New image received. Starting enhanced 2-stage pipeline...")
output_image = input_image.copy()
# --- STAGE 1: Find the license plate ---
plate_predictions = plate_model.predict(input_image, confidence=40, overlap=30).json()['predictions']
if not plate_predictions:
return output_image, "No license plate found."
# Get the highest confidence plate detection
plate_box = max(plate_predictions, key=lambda x: x['confidence'])
x1, y1, x2, y2 = [int(p) for p in [plate_box['x'] - plate_box['width'] / 2,
plate_box['y'] - plate_box['height'] / 2,
plate_box['x'] + plate_box['width'] / 2,
plate_box['y'] + plate_box['height'] / 2]]
# Optimized padding - minimal vertical to avoid extra text
h_padding = 10
v_padding = 2
y1 = max(0, y1 - v_padding)
x1 = max(0, x1 - h_padding)
y2 = min(input_image.shape[0], y2 + v_padding)
x2 = min(input_image.shape[1], x2 + h_padding)
plate_crop = input_image[y1:y2, x1:x2]
plate_height, plate_width = plate_crop.shape[:2]
# Focus on main number area (top 75% of plate)
main_number_crop = plate_crop[:int(plate_height * 0.75), :]
# --- STAGE 2: Multi-enhancement character detection ---
enhanced_crops = enhance_plate_image(main_number_crop)
all_detections = []
# Process each enhanced version with different confidence thresholds
confidence_levels = [0.25, 0.3, 0.35, 0.25] # Different thresholds for each enhancement
for i, (enhanced_crop, conf_threshold) in enumerate(zip(enhanced_crops, confidence_levels)):
try:
character_results = character_model(enhanced_crop, conf=conf_threshold, iou=0.3)
if character_results and hasattr(character_results[0], 'boxes') and len(character_results[0].boxes) > 0:
boxes = character_results[0].boxes.cpu().numpy()
filtered_detections = advanced_detection_filtering(
boxes, character_results, main_number_crop.shape, min_confidence=conf_threshold
)
print(f"Enhancement {i}: {len(boxes)} raw -> {len(filtered_detections)} filtered")
for detection in filtered_detections:
detection['enhancement_method'] = i
detection['enhancement_conf'] = conf_threshold
all_detections.append(detection)
except Exception as e:
print(f"Error processing enhancement {i}: {e}")
continue
# --- STAGE 3: Advanced ensemble voting ---
final_detections = ensemble_character_voting(all_detections, plate_width, confidence_threshold=0.3)
print(f"Ensemble voting: {len(all_detections)} total -> {len(final_detections)} final")
# --- STAGE 4: Generate and post-process text ---
if final_detections:
# Sort by x position
final_detections.sort(key=lambda x: x['center_x'])
raw_text = "".join([d['final_char'] for d in final_detections])
# Apply smart character correction
corrected_text = smart_character_correction(raw_text, pattern_analysis=True)
# Additional validation - remove obviously wrong characters
if len(corrected_text) > 8: # If too long, might have false positives
# Keep only the most confident detections
final_detections = sorted(final_detections, key=lambda x: x['final_score'], reverse=True)[:7]
final_detections.sort(key=lambda x: x['center_x'])
raw_text = "".join([d['final_char'] for d in final_detections])
corrected_text = smart_character_correction(raw_text, pattern_analysis=True)
else:
raw_text = ""
corrected_text = ""
# --- STAGE 5: Draw results ---
# Draw the main plate box
cv2.rectangle(output_image, (x1, y1), (x2, y2), (0, 0, 255), 2)
cv2.putText(output_image, f"Plate: {plate_box['confidence']:.1f}%",
(x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
# Draw detection area boundary
main_area_y = y1 + int(plate_height * 0.75)
cv2.line(output_image, (x1, main_area_y), (x2, main_area_y), (255, 255, 0), 2)
cv2.putText(output_image, "Detection Area", (x1, main_area_y - 5),
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 0), 1)
# Draw character detections
for i, detection in enumerate(final_detections):
abs_x1 = x1 + int(detection['x1'])
abs_y1 = y1 + int(detection['y1'])
abs_x2 = x1 + int(detection['x2'])
abs_y2 = y1 + int(detection['y2'])
# Color code by confidence
color = (0, 255, 0) if detection['final_score'] > 0.7 else (0, 255, 255)
cv2.rectangle(output_image, (abs_x1, abs_y1), (abs_x2, abs_y2), color, 2)
cv2.putText(output_image, f"{detection['final_char']}",
(abs_x1, abs_y1 - 15), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
cv2.putText(output_image, f"{detection['final_score']:.2f}",
(abs_x1, abs_y1 - 3), cv2.FONT_HERSHEY_SIMPLEX, 0.4, color, 1)
# Prepare result text
if raw_text != corrected_text and corrected_text:
result_text = f"Detected: {raw_text}\nCorrected: {corrected_text}\nConfidence: {len(final_detections)} chars"
elif corrected_text:
result_text = f"Result: {corrected_text}\nConfidence: {len(final_detections)} characters detected"
else:
result_text = "No characters detected with sufficient confidence"
print(f"Final result: {result_text}")
return output_image, result_text
# --- 5. Create the Gradio Web Interface ---
with gr.Blocks() as demo:
gr.Markdown("# Enhanced High-Accuracy License Plate Detector")
gr.Markdown("""
**Improved Features:**
- Advanced statistical filtering with spatial clustering
- Smart character correction based on license plate patterns
- Enhanced ensemble voting with confidence weighting
- Optimized detection area focusing
- Multi-level confidence thresholds
""")
with gr.Row():
image_input = gr.Image(type="numpy", label="Upload License Plate Image")
image_output = gr.Image(type="numpy", label="Detection Results")
text_output = gr.Textbox(label="Detected License Plate", lines=3)
predict_button = gr.Button(value="Detect License Plate", variant="primary")
predict_button.click(
fn=detect_license_plate,
inputs=image_input,
outputs=[image_output, text_output]
)
# --- 6. Launch the application ---
demo.launch()