Spaces:
Running
Running
| # --- 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() |