Ajaykumar10/Automatic_Number_Plate_Recognition_ANPR_System
0
1import gradio as gr2import cv23import numpy as np4import easyocr5from ultralytics import YOLO6import time7import re8import pandas as pd9import os10 11# ──────────────────────────────────────────────12# Configuration13# ──────────────────────────────────────────────14BASE_DIR = os.path.dirname(os.path.abspath(__file__))15YOLO_MODEL_PATH = os.path.join(BASE_DIR, "license_plate_detector.pt")16EASYOCR_MODEL_DIR = os.path.join(BASE_DIR, "models") # pre-downloaded at build time17 18MIN_ASPECT_RATIO = 2.019MAX_ASPECT_RATIO = 6.020MIN_PLATE_AREA = 150021CONFIDENCE_THRESHOLD = 0.422 23model = None24reader = None25 26 27# ──────────────────────────────────────────────28# Model Loading29# ──────────────────────────────────────────────30def load_models():31 global model, reader32 print("Loading models...")33 34 # YOLO35 if os.path.exists(YOLO_MODEL_PATH):36 try:37 model = YOLO(YOLO_MODEL_PATH)38 print("✓ YOLO model loaded")39 except Exception as e:40 print(f"⚠ YOLO failed: {e}")41 model = None42 else:43 print(f"⚠ YOLO model not found — YOLO detection disabled")44 model = None45 46 # EasyOCR — uses pre-downloaded models from /app/models/47 try:48 os.makedirs(EASYOCR_MODEL_DIR, exist_ok=True)49 reader = easyocr.Reader(50 ['en'],51 gpu=False,52 verbose=False,53 model_storage_directory=EASYOCR_MODEL_DIR,54 download_enabled=True # fallback if models folder is missing55 )56 print("✓ EasyOCR loaded")57 except Exception as e:58 print(f"✗ EasyOCR failed: {e}")59 raise60 61 return model, reader62 63 64# ══════════════════════════════════════════════65# Image Preprocessing66# ══════════════════════════════════════════════67def preprocess_image(image_bgr):68 gray = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)69 denoised = cv2.fastNlMeansDenoising(gray, h=10)70 clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))71 enhanced = clahe.apply(denoised)72 bilateral = cv2.bilateralFilter(enhanced, d=11, sigmaColor=17, sigmaSpace=17)73 blurred = cv2.GaussianBlur(bilateral, (5, 5), 0)74 return {"gray": gray, "enhanced": enhanced, "bilateral": bilateral, "blurred": blurred}75 76 77# ══════════════════════════════════════════════78# Edge Detection & Contours79# ══════════════════════════════════════════════80def detect_edges(blurred):81 median = np.median(blurred)82 sigma = 0.3383 lower = int(max(0, (1.0 - sigma) * median))84 upper = int(min(255, (1.0 + sigma) * median))85 edges = cv2.Canny(blurred, lower, upper)86 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))87 return cv2.dilate(edges, kernel, iterations=1)88 89 90def extract_contours(edges):91 contours, _ = cv2.findContours(edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)92 return sorted(contours, key=cv2.contourArea, reverse=True)[:20]93 94 95# ══════════════════════════════════════════════96# Traditional CV ROI Detection97# ══════════════════════════════════════════════98def find_plate_roi_traditional(image_bgr, contours):99 h_img, w_img = image_bgr.shape[:2]100 candidates = []101 for cnt in contours:102 area = cv2.contourArea(cnt)103 if area < MIN_PLATE_AREA:104 continue105 peri = cv2.arcLength(cnt, True)106 approx = cv2.approxPolyDP(cnt, 0.018 * peri, True)107 if len(approx) == 4:108 x, y, w, h = cv2.boundingRect(approx)109 ar = w / float(h) if h > 0 else 0110 if MIN_ASPECT_RATIO <= ar <= MAX_ASPECT_RATIO and w < 0.9 * w_img and h < 0.9 * h_img:111 candidates.append((x, y, w, h))112 return candidates113 114 115# ══════════════════════════════════════════════116# OCR117# ══════════════════════════════════════════════118def preprocess_plate_for_ocr(plate_bgr):119 h, w = plate_bgr.shape[:2]120 if w < 200:121 plate_bgr = cv2.resize(plate_bgr, None, fx=200/w, fy=200/w, interpolation=cv2.INTER_CUBIC)122 gray = cv2.cvtColor(plate_bgr, cv2.COLOR_BGR2GRAY)123 sharp = cv2.filter2D(gray, -1, np.array([[-1,-1,-1],[-1,9,-1],[-1,-1,-1]]))124 return cv2.adaptiveThreshold(sharp, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2)125 126 127def run_ocr(reader, plate_bgr):128 processed = preprocess_plate_for_ocr(plate_bgr)129 def aggregate(results):130 text, conf, count = "", 0.0, 0131 for (_, txt, c) in results:132 text += txt; conf += c; count += 1133 return text, (conf / count if count else 0.0)134 raw_text, raw_conf = aggregate(reader.readtext(plate_bgr, detail=1, paragraph=False))135 proc_text, proc_conf = aggregate(reader.readtext(processed, detail=1, paragraph=False))136 return (proc_text, proc_conf) if proc_conf >= raw_conf else (raw_text, raw_conf)137 138 139def clean_plate_text(raw):140 return re.sub(r'[^A-Za-z0-9]', '', raw).upper()141 142 143# ══════════════════════════════════════════════144# YOLO Detection145# ══════════════════════════════════════════════146def detect_with_yolo(model, image_bgr):147 results = model(image_bgr, conf=CONFIDENCE_THRESHOLD, verbose=False)148 return [(int(b.xyxy[0][0]), int(b.xyxy[0][1]), int(b.xyxy[0][2]), int(b.xyxy[0][3]), float(b.conf[0]))149 for b in results[0].boxes]150 151 152# ══════════════════════════════════════════════153# Accuracy & Drawing154# ══════════════════════════════════════════════155def character_accuracy(pred, gt):156 if not gt:157 return 1.0 if not pred else 0.0158 m, n = len(pred), len(gt)159 dp = [[0]*(n+1) for _ in range(m+1)]160 for i in range(1, m+1):161 for j in range(1, n+1):162 dp[i][j] = dp[i-1][j-1]+1 if pred[i-1]==gt[j-1] else max(dp[i-1][j], dp[i][j-1])163 return dp[m][n] / max(m, n)164 165 166def draw_result(image_bgr, x1, y1, x2, y2, label, color=(0,255,0)):167 out = image_bgr.copy()168 cv2.rectangle(out, (x1,y1), (x2,y2), color, 3)169 (tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.9, 2)170 cv2.rectangle(out, (x1, y1-th-10), (x1+tw+6, y1), color, -1)171 cv2.putText(out, label, (x1+3, y1-5), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0,0,0), 2)172 return out173 174 175# ══════════════════════════════════════════════176# Main Processing Function177# ══════════════════════════════════════════════178def process_image(image, detection_method, ground_truth):179 ensure_models_loaded()180 global model, reader181 182 if image is None:183 return None,None,None,None,None,None,"⚠ Please upload an image first.",None,None184 185 try:186 image_rgb = np.array(image) if not isinstance(image, np.ndarray) else image187 image_bgr = cv2.cvtColor(image_rgb, cv2.COLOR_RGB2BGR)188 189 # Always compute preprocessing190 prep = preprocess_image(image_bgr)191 prep1 = cv2.cvtColor(prep["gray"], cv2.COLOR_GRAY2RGB)192 prep2 = cv2.cvtColor(prep["enhanced"], cv2.COLOR_GRAY2RGB)193 prep3 = cv2.cvtColor(prep["bilateral"], cv2.COLOR_GRAY2RGB)194 195 # Always compute edges196 edges = detect_edges(prep["blurred"])197 contours = extract_contours(edges)198 edges_image = cv2.cvtColor(edges, cv2.COLOR_GRAY2RGB)199 contour_vis = image_bgr.copy()200 cv2.drawContours(contour_vis, contours, -1, (0, 120, 255), 2)201 contour_image = cv2.cvtColor(contour_vis, cv2.COLOR_BGR2RGB)202 203 final_image = image_bgr.copy()204 detections = []205 start_time = time.time()206 207 # YOLO208 if detection_method in ["YOLO (Deep Learning)", "Both"]:209 if model is None and detection_method == "YOLO (Deep Learning)":210 msg = ("⚠ **YOLO model not found.**\n\nUpload `license_plate_detector.pt` "211 "to your Space, or switch to **Traditional CV** method.")212 return prep1, prep2, prep3, edges_image, contour_image, None, msg, None, None213 elif model is not None:214 for (x1,y1,x2,y2,conf) in detect_with_yolo(model, image_bgr):215 plate_bgr = image_bgr[y1:y2, x1:x2]216 if plate_bgr.size == 0: continue217 raw_text, ocr_conf = run_ocr(reader, plate_bgr)218 detections.append({"box":(x1,y1,x2,y2),"text":clean_plate_text(raw_text),219 "det_conf":conf,"ocr_conf":ocr_conf,"method":"YOLO","plate_img":plate_bgr})220 221 # Traditional CV222 if detection_method in ["Traditional CV (Edge + Contour)", "Both"]:223 for (x,y,w,h) in find_plate_roi_traditional(image_bgr, contours):224 plate_bgr = image_bgr[y:y+h, x:x+w]225 if plate_bgr.size == 0: continue226 raw_text, ocr_conf = run_ocr(reader, plate_bgr)227 detections.append({"box":(x,y,x+w,y+h),"text":clean_plate_text(raw_text),228 "det_conf":None,"ocr_conf":ocr_conf,"method":"Traditional CV","plate_img":plate_bgr})229 230 elapsed = time.time() - start_time231 232 if not detections:233 msg = ("❌ **No license plate detected.**\n\nTry:\n"234 "• A clearer, well-lit image\n• A different detection method\n"235 "• Ensure the plate is fully visible")236 return prep1, prep2, prep3, edges_image, contour_image, None, msg, None, None237 238 for det in detections:239 x1,y1,x2,y2 = det["box"]240 color = (0,200,0) if det["method"]=="YOLO" else (200,150,0)241 final_image = draw_result(final_image, x1,y1,x2,y2, det["text"] or "???", color)242 243 final_rgb = cv2.cvtColor(final_image, cv2.COLOR_BGR2RGB)244 plate_images = [cv2.cvtColor(d["plate_img"], cv2.COLOR_BGR2RGB) for d in detections]245 246 avg_conf = np.mean([d["ocr_conf"] for d in detections]) * 100247 result_text = (f"⏱ **Processing Time:** {elapsed:.2f}s\n"248 f"📦 **Plates Detected:** {len(detections)}\n"249 f"🔤 **Avg OCR Confidence:** {avg_conf:.1f}%\n\n")250 for i, det in enumerate(detections):251 result_text += f"**Plate #{i+1}:**\n- Text: `{det['text'] or 'N/A'}`\n- Method: {det['method']}\n"252 if det["det_conf"]: result_text += f"- Detection Conf: {det['det_conf']*100:.1f}%\n"253 result_text += f"- OCR Conf: {det['ocr_conf']*100:.1f}%\n\n"254 255 if ground_truth and ground_truth.strip():256 gt_clean = clean_plate_text(ground_truth)257 result_text += f"\n✅ **Ground Truth:** `{gt_clean}`\n"258 for i, det in enumerate(detections):259 acc = character_accuracy(det["text"], gt_clean)260 match = "✅ Exact Match" if det["text"]==gt_clean else "❌ Mismatch"261 result_text += f"- Plate #{i+1}: {acc*100:.1f}% | {match}\n"262 263 df = pd.DataFrame([{264 "Plate #": i+1, "Method": d["method"],265 "Recognized Text": d["text"] or "N/A",266 "OCR Conf (%)": f"{d['ocr_conf']*100:.1f}",267 "Det Conf (%)": f"{d['det_conf']*100:.1f}" if d["det_conf"] else "N/A",268 } for i,d in enumerate(detections)])269 270 return prep1, prep2, prep3, edges_image, contour_image, final_rgb, result_text, df, plate_images271 272 except Exception as e:273 return None,None,None,None,None,None,f"❌ Error: {str(e)}",None,None274 275 276# ══════════════════════════════════════════════277# Gradio Interface278# ══════════════════════════════════════════════279def create_interface():280 with gr.Blocks(title="ANPR System") as demo:281 gr.Markdown("""282 # 🚗 Automatic Number Plate Recognition (ANPR)283 ### Detect and recognize license plates using AI and Computer Vision284 Upload a vehicle image and click **Detect License Plate** to begin.285 """)286 287 with gr.Row():288 with gr.Column(scale=1):289 gr.Markdown("### ⚙️ Configuration")290 image_input = gr.Image(label="📷 Upload Vehicle Image", type="numpy", height=300)291 detection_method = gr.Radio(292 choices=["YOLO (Deep Learning)", "Traditional CV (Edge + Contour)", "Both"],293 value="Traditional CV (Edge + Contour)",294 label="Detection Method",295 info="YOLO requires license_plate_detector.pt in the Space"296 )297 with gr.Accordion("Advanced Options", open=False):298 ground_truth = gr.Textbox(299 label="Ground Truth Plate (Optional)",300 placeholder="e.g., SN66XMZ",301 info="Enter correct plate text to evaluate accuracy"302 )303 process_btn = gr.Button("🔍 Detect License Plate", variant="primary", size="lg")304 gr.Markdown("""305 ---306 ### 📝 Tips:307 - Use clear, well-lit images308 - **Traditional CV** works without any model309 - **YOLO** needs `license_plate_detector.pt` in Space310 """)311 312 with gr.Column(scale=2):313 with gr.Tabs():314 with gr.Tab("🎯 Results"):315 final_output = gr.Image(label="Detected License Plates")316 result_text = gr.Markdown(value="📌 Upload an image and click **Detect License Plate** to begin.")317 summary_table = gr.Dataframe(318 headers=["Plate #","Method","Recognized Text","OCR Conf (%)","Det Conf (%)"],319 label="Detection Summary"320 )321 with gr.Tab("🔍 Detected Plates"):322 plate_gallery = gr.Gallery(label="Cropped License Plates", columns=3, height="auto", object_fit="contain")323 with gr.Tab("🔧 Preprocessing"):324 gr.Markdown("**Always shown after clicking Detect.**")325 with gr.Row():326 prep_col1 = gr.Image(label="Grayscale")327 prep_col2 = gr.Image(label="CLAHE Enhanced")328 prep_col3 = gr.Image(label="Bilateral Filtered")329 with gr.Tab("📐 Edge Detection"):330 gr.Markdown("**Always shown after clicking Detect.**")331 with gr.Row():332 edges_output = gr.Image(label="Canny Edges")333 contour_output = gr.Image(label="Contour Overlay")334 335 process_btn.click(336 fn=process_image,337 inputs=[image_input, detection_method, ground_truth],338 outputs=[prep_col1, prep_col2, prep_col3, edges_output, contour_output,339 final_output, result_text, summary_table, plate_gallery]340 )341 342 gr.Markdown("""343 ---344 <div style="text-align:center;color:#666;">345 <p><strong>ANPR System</strong> | Gradio · OpenCV · EasyOCR · YOLOv8</p>346 <p>Mini Project – Language & Libraries for Technology (LLT)</p>347 </div>348 """)349 return demo350 351 352# ══════════════════════════════════════════════353# Entry Point354# ══════════════════════════════════════════════355def ensure_models_loaded():356 global model, reader357 if model is None or reader is None:358 load_models()359demo = create_interface()360import os361 362port = int(os.environ.get("PORT", 7860))363 364if __name__ == "__main__":365 demo.launch(366 server_name="0.0.0.0",367 server_port=port368)