# --- 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 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=3.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.5, blurred, -0.5, 0) unsharp = np.clip(unsharp, 0, 255).astype(np.uint8) enhanced_crops.append(cv2.cvtColor(unsharp, cv2.COLOR_GRAY2RGB)) # Enhancement 3: Morphological operations kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2)) morph = cv2.morphologyEx(gray, cv2.MORPH_CLOSE, kernel) enhanced_crops.append(cv2.cvtColor(morph, cv2.COLOR_GRAY2RGB)) # Enhancement 4: Bilateral filter bilateral = cv2.bilateralFilter(gray, 9, 75, 75) enhanced_crops.append(cv2.cvtColor(bilateral, cv2.COLOR_GRAY2RGB)) return enhanced_crops def post_process_text(raw_text): """ Apply license plate specific formatting and corrections """ if not raw_text: return raw_text # Remove any spaces first text = raw_text.replace(" ", "") # Common character corrections for license plates corrections = { '0': 'O', # In letter context 'O': '0', # In number context 'I': '1', '1': 'I', 'S': '5', '5': 'S', 'Z': '2', 'B': '8', '8': 'B', 'G': '6', '6': 'G' } # For Philippine plates, common format is 3 letters + 3 numbers (like NOV706) if len(text) >= 6: corrected_chars = list(text) # First 3 should typically be letters for i in range(min(3, len(corrected_chars))): char = corrected_chars[i] if char.isdigit(): # Convert common digit misreads to letters if char in ['0', '1', '5', '8']: letter_map = {'0': 'O', '1': 'I', '5': 'S', '8': 'B'} corrected_chars[i] = letter_map.get(char, char) # Last 3 should typically be numbers for i in range(3, min(6, len(corrected_chars))): char = corrected_chars[i] if char.isalpha(): # Convert common letter misreads to numbers if char in ['O', 'I', 'S', 'B', 'G', 'Z']: number_map = {'O': '0', 'I': '1', 'S': '5', 'B': '8', 'G': '6', 'Z': '2'} corrected_chars[i] = number_map.get(char, char) text = ''.join(corrected_chars) return text def improved_filtering(boxes, character_results, plate_crop_shape, min_confidence=0.3): """ Enhanced filtering focusing on main license plate number only """ 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) }) if len(detections) == 0: return [] # MAIN IMPROVEMENT: Focus on the upper portion of the plate # Most license plates have the main number in the top 60% of the plate plate_height = plate_crop_shape[0] upper_threshold = plate_height * 0.70 # Only consider top 65% of plate # Filter out detections in lower portion (subsidiary text area) upper_detections = [d for d in detections if d['center_y'] <= upper_threshold] if len(upper_detections) == 0: # Fallback: if no detections in upper area, use all but be more selective upper_detections = detections print("Warning: No detections in upper area, using all detections") print(f"Filtered to upper area: {len(upper_detections)}/{len(detections)} detections") # Calculate statistics for filtering (now only on upper detections) heights = [d['height'] for d in upper_detections] widths = [d['width'] for d in upper_detections] y_centers = [d['center_y'] for d in upper_detections] if len(heights) == 0: return [] median_height = np.median(heights) median_width = np.median(widths) median_y_center = np.median(y_centers) # More aggressive filtering for main plate numbers filtered_detections = [] for detection in upper_detections: # Size filtering (tighter for main numbers) height_ratio = detection['height'] / median_height width_ratio = detection['width'] / median_width # Alignment filtering (tighter) y_deviation = abs(detection['center_y'] - median_y_center) max_y_deviation = median_height * 0.4 # Reduced from 0.6 to 0.4 # Height-based filtering: main numbers are usually larger min_height_threshold = plate_height * 0.15 # At least 15% of plate height # Keep detection if it passes all criteria if (0.5 <= height_ratio <= 2.0 and # Tighter height range 0.4 <= width_ratio <= 2.5 and # Tighter width range y_deviation <= max_y_deviation and # Better alignment detection['height'] >= min_height_threshold): # Minimum size filtered_detections.append(detection) return filtered_detections # --- 4. Enhanced main prediction function --- def detect_license_plate(input_image): """ Enhanced version with multi-enhancement processing 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]] # Add some padding to the plate crop, but reduce vertical padding to avoid extra text h_padding = 8 # Horizontal padding v_padding = 3 # Minimal vertical padding to avoid bottom text 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] # Crop to focus on upper portion where main numbers are located plate_height = plate_crop.shape[0] # Keep top 70% of the plate to exclude bottom text area main_number_crop = plate_crop[:int(plate_height * 0.7), :] # --- STAGE 2: Multi-enhancement character detection --- enhanced_crops = enhance_plate_image(main_number_crop) all_detections = [] character_votes = {} # Process each enhanced version for i, enhanced_crop in enumerate(enhanced_crops): try: character_results = character_model(enhanced_crop, conf=0.3, iou=0.4) 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 = improved_filtering(boxes, character_results, main_number_crop.shape, min_confidence=0.3) print(f"Enhancement {i}: {len(boxes)} raw -> {len(filtered_detections)} filtered detections") for detection in filtered_detections: # Add enhancement method info detection['enhancement'] = i all_detections.append(detection) # Collect votes for ensemble x_pos = int(detection['center_x'] / 8) * 8 # Tighter grouping key = f"x{x_pos}" if key not in character_votes: character_votes[key] = [] character_votes[key].append((detection['char'], detection['conf'])) except Exception as e: print(f"Error processing enhancement {i}: {e}") continue # --- STAGE 3: Ensemble voting and final selection --- final_detections = [] if character_votes: for x_key in sorted(character_votes.keys()): votes = character_votes[x_key] # Weight votes by confidence and count char_scores = {} for char, conf in votes: if char not in char_scores: char_scores[char] = [] char_scores[char].append(conf) # Calculate weighted scores best_char = None best_score = 0 for char, confs in char_scores.items(): # Score = average confidence * count weight avg_conf = np.mean(confs) count_weight = min(len(confs) / len(enhanced_crops), 1.0) score = avg_conf * (0.7 + 0.3 * count_weight) if score > best_score: best_score = score best_char = char if best_char and best_score > 0.3: # Find representative detection for drawing x_pos = int(x_key[1:]) representative = min([d for d in all_detections if abs(d['center_x'] - x_pos) < 15], key=lambda x: abs(x['center_x'] - x_pos), default=None) if representative: representative['final_char'] = best_char representative['final_conf'] = best_score final_detections.append(representative) # --- STAGE 4: Draw results and generate text --- # Draw the main plate box cv2.rectangle(output_image, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(output_image, f"Plate Conf: {plate_box['confidence']:.2f}", (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) # Draw character boxes (adjust coordinates back to main number crop area) for detection in 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']) cv2.rectangle(output_image, (abs_x1, abs_y1), (abs_x2, abs_y2), (0, 255, 0), 2) cv2.putText(output_image, f"{detection['final_char']} {detection['final_conf']:.2f}", (abs_x1, abs_y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) # Draw a line to show the detection area boundary main_area_y = y1 + int(plate_height * 0.7) cv2.line(output_image, (x1, main_area_y), (x2, main_area_y), (255, 255, 0), 2) # Sort by x position and create final text final_detections.sort(key=lambda x: x['center_x']) raw_text = "".join([d['final_char'] for d in final_detections]) # Apply post-processing final_text = post_process_text(raw_text) result_text = f"Raw: {raw_text}\nProcessed: {final_text}" if raw_text != final_text else final_text print(f"Prediction complete. Final result: {result_text}") print(f"Used {len(final_detections)} characters from {len(all_detections)} total detections") 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(""" This system uses an advanced 2-stage AI pipeline with: - Multiple image enhancement techniques - Ensemble voting across different processed versions - Smart filtering and post-processing - Common license plate character corrections """) 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 Characters", lines=3) predict_button = gr.Button(value="Detect Characters", variant="primary") predict_button.click( fn=detect_license_plate, inputs=image_input, outputs=[image_output, text_output] ) # --- 6. Launch the application --- demo.launch()