Spaces:
Build error
Build error
Ayush commited on
Commit ·
f02eb0f
1
Parent(s): 85c89a4
Added files
Browse files- Dockerfile +31 -0
- app.py +274 -0
- best.pt +3 -0
- easyocr_models/craft_mlt_25k.pth +3 -0
- easyocr_models/english_g2.pth +3 -0
- readme copy.md +60 -0
- requirements.txt +6 -0
- templates/index.html +53 -0
- templates/result.html +74 -0
- test.jpeg +0 -0
- test2.jpeg +0 -0
- yolov8n.pt +3 -0
Dockerfile
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use a base Python image
|
| 2 |
+
FROM python:3.9-slim
|
| 3 |
+
|
| 4 |
+
# Set the working directory
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Copy requirements first to leverage Docker caching
|
| 8 |
+
COPY requirements.txt requirements.txt
|
| 9 |
+
|
| 10 |
+
# Install system dependencies needed by OpenCV and potentially others
|
| 11 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 12 |
+
libgl1-mesa-glx \
|
| 13 |
+
libglib2.0-0 \
|
| 14 |
+
# Add any other system libs if needed during build
|
| 15 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 16 |
+
|
| 17 |
+
# Install Python dependencies
|
| 18 |
+
# Use --no-cache-dir to reduce image size
|
| 19 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 20 |
+
|
| 21 |
+
# Copy the rest of the application code
|
| 22 |
+
COPY . .
|
| 23 |
+
|
| 24 |
+
# Expose the port Flask runs on (default is 5000)
|
| 25 |
+
EXPOSE 5000
|
| 26 |
+
|
| 27 |
+
# Command to run the application
|
| 28 |
+
# Use gunicorn for a more robust server than Flask's default debug server
|
| 29 |
+
# Install gunicorn first
|
| 30 |
+
RUN pip install --no-cache-dir gunicorn
|
| 31 |
+
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]
|
app.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import time
|
| 3 |
+
import cv2 # OpenCV for image handling
|
| 4 |
+
import easyocr # For OCR
|
| 5 |
+
import torch # PyTorch (dependency for YOLO/EasyOCR)
|
| 6 |
+
import numpy as np # For numerical operations
|
| 7 |
+
from ultralytics import YOLO # YOLO model from ultralytics
|
| 8 |
+
from flask import Flask, request, render_template, redirect, url_for, flash
|
| 9 |
+
from werkzeug.utils import secure_filename
|
| 10 |
+
import traceback # To print full error tracebacks
|
| 11 |
+
import re # Import regular expressions for post-processing
|
| 12 |
+
|
| 13 |
+
# --- Configuration ---
|
| 14 |
+
UPLOAD_FOLDER = os.path.join('static', 'uploads')
|
| 15 |
+
RESULT_FOLDER = os.path.join('static', 'results')
|
| 16 |
+
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg'}
|
| 17 |
+
|
| 18 |
+
weights_path = 'best.pt' # Assumes best.pt is in the same folder as app.py
|
| 19 |
+
PLATE_CLASS_NAME = 'license_plate'
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
app = Flask(__name__)
|
| 23 |
+
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
| 24 |
+
app.config['RESULT_FOLDER'] = RESULT_FOLDER
|
| 25 |
+
app.secret_key = 'super secret key' # Needed for flashing messages
|
| 26 |
+
|
| 27 |
+
# --- Create Folders if they don't exist ---
|
| 28 |
+
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
| 29 |
+
os.makedirs(RESULT_FOLDER, exist_ok=True)
|
| 30 |
+
|
| 31 |
+
# --- Load Models (Load only once when the app starts) ---
|
| 32 |
+
reader = None
|
| 33 |
+
model = None
|
| 34 |
+
model_class_names = {} # Initialize empty
|
| 35 |
+
|
| 36 |
+
print("Loading EasyOCR Reader...")
|
| 37 |
+
try:
|
| 38 |
+
# Use gpu=True if you have a CUDA-enabled GPU and compatible PyTorch/CUDA
|
| 39 |
+
model_dir = os.path.join('.', 'easyocr_models')
|
| 40 |
+
os.makedirs(model_dir, exist_ok=True)
|
| 41 |
+
reader = easyocr.Reader(['en'], gpu=torch.cuda.is_available(), model_storage_directory=model_dir)
|
| 42 |
+
print("EasyOCR Reader loaded successfully.")
|
| 43 |
+
except Exception as e:
|
| 44 |
+
print(f"Error loading EasyOCR Reader: {e}. OCR will not work.")
|
| 45 |
+
print(traceback.format_exc()) # Print full traceback
|
| 46 |
+
|
| 47 |
+
print("Loading YOLO Model...")
|
| 48 |
+
try:
|
| 49 |
+
if not os.path.exists(weights_path):
|
| 50 |
+
raise FileNotFoundError(f"YOLO weights file not found at {weights_path}. Please make sure 'best.pt' is in the same directory as app.py or update the path.")
|
| 51 |
+
model = YOLO(weights_path)
|
| 52 |
+
print("YOLO Model loaded successfully.")
|
| 53 |
+
# Store class names if model loaded
|
| 54 |
+
if model:
|
| 55 |
+
model_class_names = model.names
|
| 56 |
+
print("Class names:", model_class_names) # Print class names {index: 'name'}
|
| 57 |
+
except Exception as e:
|
| 58 |
+
print(f"Error loading YOLO model from {weights_path}: {e}. Detection will not work.")
|
| 59 |
+
print(traceback.format_exc()) # Print full traceback
|
| 60 |
+
|
| 61 |
+
# --- Helper Functions ---
|
| 62 |
+
def allowed_file(filename):
|
| 63 |
+
"""Checks if the filename has an allowed extension."""
|
| 64 |
+
return '.' in filename and \
|
| 65 |
+
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
| 66 |
+
|
| 67 |
+
def perform_ocr_on_image(img_crop, allowlist='0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'):
|
| 68 |
+
"""Performs OCR on a given image crop (NumPy array)."""
|
| 69 |
+
if reader is None:
|
| 70 |
+
print("EasyOCR Reader not loaded. Cannot perform OCR.")
|
| 71 |
+
return "OCR Error"
|
| 72 |
+
|
| 73 |
+
try:
|
| 74 |
+
# Convert to grayscale
|
| 75 |
+
gray_img = cv2.cvtColor(img_crop, cv2.COLOR_BGR2GRAY)
|
| 76 |
+
|
| 77 |
+
# Resize for consistent input size
|
| 78 |
+
target_height = 64 # You can adjust this value
|
| 79 |
+
scale_ratio = target_height / gray_img.shape[0]
|
| 80 |
+
target_width = int(gray_img.shape[1] * scale_ratio)
|
| 81 |
+
resized_img = cv2.resize(gray_img, (target_width, target_height))
|
| 82 |
+
|
| 83 |
+
# Basic preprocessing
|
| 84 |
+
resized_img = cv2.equalizeHist(resized_img) # Enhance contrast
|
| 85 |
+
|
| 86 |
+
# Perform OCR
|
| 87 |
+
results = reader.readtext(resized_img, allowlist=allowlist)
|
| 88 |
+
|
| 89 |
+
# Process results
|
| 90 |
+
if not results:
|
| 91 |
+
return "No text found"
|
| 92 |
+
|
| 93 |
+
# Take the result with highest confidence
|
| 94 |
+
text = results[0][1] # Get the text part
|
| 95 |
+
|
| 96 |
+
# Convert to uppercase and remove any non-alphanumeric characters
|
| 97 |
+
text = text.upper()
|
| 98 |
+
text = re.sub(r'[^A-Z0-9]', '', text)
|
| 99 |
+
|
| 100 |
+
return text if text else "No text found"
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
print(f"Final combined text after post-processing: '{plate_text}'")
|
| 104 |
+
|
| 105 |
+
return plate_text if plate_text else "No text found"
|
| 106 |
+
|
| 107 |
+
except Exception as e:
|
| 108 |
+
print(f"Error during OCR processing in perform_ocr_on_image: {e}")
|
| 109 |
+
print(traceback.format_exc())
|
| 110 |
+
return "OCR Processing Error"
|
| 111 |
+
|
| 112 |
+
# ... (rest of run_detection_ocr, Flask routes, and __main__ remain the same) ...
|
| 113 |
+
|
| 114 |
+
def run_detection_ocr(image_path, result_filename):
|
| 115 |
+
"""Runs YOLO detection and EasyOCR on the image."""
|
| 116 |
+
if model is None or reader is None:
|
| 117 |
+
print("Models not loaded. Cannot process image.")
|
| 118 |
+
return None, ["Model Loading Error"]
|
| 119 |
+
|
| 120 |
+
detected_texts = []
|
| 121 |
+
result_image_path = None
|
| 122 |
+
|
| 123 |
+
try:
|
| 124 |
+
# Read the image
|
| 125 |
+
img = cv2.imread(image_path)
|
| 126 |
+
if img is None:
|
| 127 |
+
print(f"Error: Could not read image at {image_path}")
|
| 128 |
+
return None, ["Image Reading Error"]
|
| 129 |
+
|
| 130 |
+
img_copy = img.copy() # Make a copy for drawing
|
| 131 |
+
|
| 132 |
+
# Run YOLO detection with original confidence threshold
|
| 133 |
+
print(f"Running YOLO prediction on {image_path}...")
|
| 134 |
+
results = model.predict(img, conf=0.25) # Restore original confidence for detection
|
| 135 |
+
print("YOLO prediction complete.")
|
| 136 |
+
|
| 137 |
+
# Process detections
|
| 138 |
+
for result in results:
|
| 139 |
+
boxes = result.boxes
|
| 140 |
+
if boxes is None or len(boxes) == 0:
|
| 141 |
+
print("No boxes detected in this result.")
|
| 142 |
+
continue
|
| 143 |
+
|
| 144 |
+
for box in boxes:
|
| 145 |
+
try:
|
| 146 |
+
# Get class index and name using the loaded names
|
| 147 |
+
class_id = int(box.cls[0])
|
| 148 |
+
class_name = model_class_names.get(class_id, 'Unknown')
|
| 149 |
+
confidence = float(box.conf[0])
|
| 150 |
+
x1_temp, y1_temp, x2_temp, y2_temp = map(int, box.xyxy[0])
|
| 151 |
+
print(f"Detected '{class_name}' (Conf: {confidence:.2f}) at [{x1_temp}, {y1_temp}, {x2_temp}, {y2_temp}]")
|
| 152 |
+
|
| 153 |
+
# Check with the CORRECTED class name
|
| 154 |
+
if class_name.lower() == PLATE_CLASS_NAME.lower(): # Comparison should now work
|
| 155 |
+
x1, y1, x2, y2 = map(int, box.xyxy[0])
|
| 156 |
+
padding = 10
|
| 157 |
+
crop_y1 = max(0, y1 - padding)
|
| 158 |
+
crop_y2 = min(img.shape[0], y2 + padding)
|
| 159 |
+
crop_x1 = max(0, x1 - padding)
|
| 160 |
+
crop_x2 = min(img.shape[1], x2 + padding)
|
| 161 |
+
|
| 162 |
+
if crop_y2 > crop_y1 and crop_x2 > crop_x1:
|
| 163 |
+
plate_crop = img[crop_y1:crop_y2, crop_x1:crop_x2]
|
| 164 |
+
|
| 165 |
+
# Added print before calling OCR
|
| 166 |
+
print(f"--- Cropped {PLATE_CLASS_NAME} at [{crop_x1}, {crop_y1}, {crop_x2}, {crop_y2}], attempting OCR ---")
|
| 167 |
+
plate_text = perform_ocr_on_image(plate_crop) # Function updated with preprocessing/postprocessing
|
| 168 |
+
print(f"-> OCR Result returned for {PLATE_CLASS_NAME}: {plate_text}")
|
| 169 |
+
detected_texts.append(plate_text)
|
| 170 |
+
|
| 171 |
+
# Drawing logic
|
| 172 |
+
label = f"{class_name}: {plate_text}" # Label uses result from OCR call
|
| 173 |
+
cv2.rectangle(img_copy, (x1, y1), (x2, y2), (0, 255, 0), 2)
|
| 174 |
+
text_pos = (x1, y1 - 10) if y1 > 20 else (x1, y2 + 20)
|
| 175 |
+
cv2.putText(img_copy, label, text_pos,
|
| 176 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
|
| 177 |
+
else:
|
| 178 |
+
print(f"Skipping invalid crop dimensions for {PLATE_CLASS_NAME}: y1={crop_y1}, y2={crop_y2}, x1={crop_x1}, x2={crop_x2}")
|
| 179 |
+
else:
|
| 180 |
+
# Draw blue boxes for other detected objects (optional)
|
| 181 |
+
cv2.rectangle(img_copy, (x1_temp, y1_temp), (x2_temp, y2_temp), (255, 0, 0), 1) # Blue box, thinner
|
| 182 |
+
cv2.putText(img_copy, f"{class_name} {confidence:.2f}", (x1_temp, y1_temp - 5),
|
| 183 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 1)
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
except Exception as e_inner:
|
| 187 |
+
print(f"Error processing a specific detection: {e_inner}")
|
| 188 |
+
print(traceback.format_exc())
|
| 189 |
+
detected_texts.append("Detection Proc. Error")
|
| 190 |
+
|
| 191 |
+
# Save the result image
|
| 192 |
+
result_image_path = os.path.join(app.config['RESULT_FOLDER'], result_filename)
|
| 193 |
+
cv2.imwrite(result_image_path, img_copy)
|
| 194 |
+
print(f"Result image saved to: {result_image_path}")
|
| 195 |
+
|
| 196 |
+
return result_image_path, detected_texts
|
| 197 |
+
|
| 198 |
+
except Exception as e:
|
| 199 |
+
print(f"Error during detection/OCR: {e}")
|
| 200 |
+
print(traceback.format_exc())
|
| 201 |
+
return None, ["Overall Processing Error"]
|
| 202 |
+
|
| 203 |
+
# --- Flask Routes ---
|
| 204 |
+
@app.route('/', methods=['GET', 'POST'])
|
| 205 |
+
def upload_file():
|
| 206 |
+
error_message = None
|
| 207 |
+
if request.method == 'POST':
|
| 208 |
+
if 'file' not in request.files:
|
| 209 |
+
flash('No file part')
|
| 210 |
+
return redirect(request.url)
|
| 211 |
+
file = request.files['file']
|
| 212 |
+
if file.filename == '':
|
| 213 |
+
flash('No selected file')
|
| 214 |
+
return redirect(request.url)
|
| 215 |
+
|
| 216 |
+
if file and allowed_file(file.filename):
|
| 217 |
+
try:
|
| 218 |
+
original_filename = secure_filename(file.filename)
|
| 219 |
+
timestamp = time.strftime("%Y%m%d-%H%M%S")
|
| 220 |
+
unique_filename = f"{timestamp}_{original_filename}"
|
| 221 |
+
upload_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
|
| 222 |
+
file.save(upload_path)
|
| 223 |
+
print(f"File saved to: {upload_path}")
|
| 224 |
+
|
| 225 |
+
# --- Call Processing Logic ---
|
| 226 |
+
if model is None or reader is None:
|
| 227 |
+
return render_template('result.html',
|
| 228 |
+
original_image_url=url_for('static', filename=f'uploads/{unique_filename}'),
|
| 229 |
+
result_image_url=None,
|
| 230 |
+
detected_texts=None,
|
| 231 |
+
error_message="AI models failed to load. Cannot process image.")
|
| 232 |
+
|
| 233 |
+
result_filename = f"result_{unique_filename}"
|
| 234 |
+
result_image_path, detected_texts = run_detection_ocr(upload_path, result_filename)
|
| 235 |
+
|
| 236 |
+
# Generate URLs for the templates
|
| 237 |
+
original_url = url_for('static', filename=f'uploads/{unique_filename}')
|
| 238 |
+
result_url = url_for('static', filename=f'results/{result_filename}') if result_image_path else None
|
| 239 |
+
|
| 240 |
+
# *** Filter out error messages before displaying ***
|
| 241 |
+
display_texts = [text for text in detected_texts if "Error" not in text and text != "No text found" and text != "Crop too small" and text != "Invalid Crop"]
|
| 242 |
+
if not display_texts and detected_texts: # If only errors or 'no text', show generic message
|
| 243 |
+
display_texts = ["No text recognized"]
|
| 244 |
+
elif not detected_texts and not display_texts: # If detection worked but OCR returned nothing valid
|
| 245 |
+
display_texts = ["No text recognized"]
|
| 246 |
+
elif not detected_texts: # Fallback if processing completely failed
|
| 247 |
+
display_texts = ["Processing Error"]
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
return render_template('result.html',
|
| 251 |
+
original_image_url=original_url,
|
| 252 |
+
result_image_url=result_url,
|
| 253 |
+
detected_texts=display_texts, # Use filtered list
|
| 254 |
+
error_message=None if result_image_path else "An error occurred during image processing.")
|
| 255 |
+
|
| 256 |
+
except Exception as e:
|
| 257 |
+
error_message = f"An error occurred during file upload or processing: {e}"
|
| 258 |
+
print(f"Error in upload_file route: {e}")
|
| 259 |
+
print(traceback.format_exc())
|
| 260 |
+
# Render index again, show the error
|
| 261 |
+
return render_template('index.html', error_message=error_message)
|
| 262 |
+
|
| 263 |
+
else:
|
| 264 |
+
error_message = "Invalid file type. Please upload a PNG, JPG, or JPEG image."
|
| 265 |
+
return render_template('index.html', error_message=error_message)
|
| 266 |
+
|
| 267 |
+
# For GET requests
|
| 268 |
+
return render_template('index.html', error_message=None)
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
# --- Run the App ---
|
| 272 |
+
if __name__ == "__main__":
|
| 273 |
+
app.run(debug=True)
|
| 274 |
+
|
best.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2d95861825bb4184404344c9cf809f40fd31dba785fe54e8ba5b9a3583789822
|
| 3 |
+
size 6248291
|
easyocr_models/craft_mlt_25k.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4a5efbfb48b4081100544e75e1e2b57f8de3d84f213004b14b85fd4b3748db17
|
| 3 |
+
size 83152330
|
easyocr_models/english_g2.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e2272681d9d67a04e2dff396b6e95077bc19001f8f6d3593c307b9852e1c29e8
|
| 3 |
+
size 15143997
|
readme copy.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Automatic Number Plate Recognition
|
| 3 |
+
emoji: 🚗
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: docker # Using Docker for more control with OpenCV/PyTorch
|
| 7 |
+
pinned: false
|
| 8 |
+
app_file: app.py # Tells Spaces which file to run
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# 🚗 Automatic Number Plate Recognition (ANPR)
|
| 12 |
+
|
| 13 |
+
This **Hugging Face Space** demonstrates an **Automatic Number Plate Recognition (ANPR)** system built using **Flask**, **YOLOv9**, and **EasyOCR**.
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
## 🔍 Features
|
| 18 |
+
|
| 19 |
+
- **Detection:** [YOLOv9](https://github.com/ultralytics/ultralytics) via the Ultralytics library, trained specifically on license plates.
|
| 20 |
+
- **OCR:** [EasyOCR](https://github.com/JaidedAI/EasyOCR) to extract text from detected plates.
|
| 21 |
+
- **Framework:** Flask-based web application for easy upload and visualization.
|
| 22 |
+
|
| 23 |
+
---
|
| 24 |
+
|
| 25 |
+
## 🧠 How It Works
|
| 26 |
+
|
| 27 |
+
1. **Upload an Image:**
|
| 28 |
+
Click **“Choose File”** to select a `.png`, `.jpg`, or `.jpeg` image containing a vehicle.
|
| 29 |
+
|
| 30 |
+
2. **Process the Image:**
|
| 31 |
+
Click **“Upload and Process”**.
|
| 32 |
+
|
| 33 |
+
3. **View Results:**
|
| 34 |
+
- Original image
|
| 35 |
+
- Processed image with the detected plate **boxed and labeled**
|
| 36 |
+
- Extracted **plate text** displayed below
|
| 37 |
+
|
| 38 |
+
---
|
| 39 |
+
|
| 40 |
+
## ⚙️ Architecture Overview
|
| 41 |
+
|
| 42 |
+
| Component | Technology | Purpose |
|
| 43 |
+
|------------|-------------|----------|
|
| 44 |
+
| **Detection Model** | YOLOv9 | Detects license plate region |
|
| 45 |
+
| **OCR Engine** | EasyOCR | Extracts alphanumeric text |
|
| 46 |
+
| **Framework** | Flask | Provides web interface |
|
| 47 |
+
| **Containerization** | Docker | Ensures reproducible environment |
|
| 48 |
+
|
| 49 |
+
---
|
| 50 |
+
|
| 51 |
+
## 📦 Installation & Setup
|
| 52 |
+
|
| 53 |
+
```bash
|
| 54 |
+
# Clone this repository
|
| 55 |
+
git clone https://huggingface.co/spaces/your-username/anpr
|
| 56 |
+
cd anpr
|
| 57 |
+
|
| 58 |
+
# Build and run using Docker
|
| 59 |
+
docker build -t anpr .
|
| 60 |
+
docker run -p 7860:7860 anpr
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Flask>=2.0
|
| 2 |
+
opencv-python-headless
|
| 3 |
+
easyocr
|
| 4 |
+
ultralytics
|
| 5 |
+
torch
|
| 6 |
+
torchvision
|
templates/index.html
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>ANPR Image Upload</title>
|
| 8 |
+
<!-- Load Tailwind CSS from CDN -->
|
| 9 |
+
<script src="https://www.google.com/search?q=https://cdn.tailwindcss.com"></script>
|
| 10 |
+
<style>
|
| 11 |
+
/* Optional: Add custom styles or use a font like Inter */
|
| 12 |
+
body {
|
| 13 |
+
font-family: 'Inter', sans-serif;
|
| 14 |
+
}
|
| 15 |
+
</style>
|
| 16 |
+
</head>
|
| 17 |
+
|
| 18 |
+
<body class="bg-gray-100 flex items-center justify-center min-h-screen">
|
| 19 |
+
<div class="bg-white p-8 rounded-lg shadow-xl w-full max-w-lg">
|
| 20 |
+
<h1 class="text-3xl font-bold mb-6 text-center text-gray-800">Number Plate Detector</h1>
|
| 21 |
+
<p class="text-center text-gray-600 mb-6">Upload an image containing a vehicle's number plate.</p>
|
| 22 |
+
|
| 23 |
+
<!-- The form sends data to our '/' route using the POST method -->
|
| 24 |
+
<!-- enctype="multipart/form-data" is essential for file uploads -->
|
| 25 |
+
<form method="post" enctype="multipart/form-data" class="space-y-6">
|
| 26 |
+
<div>
|
| 27 |
+
<label for="file" class="block text-sm font-medium text-gray-700 mb-2">Choose an image file:</label>
|
| 28 |
+
<input type="file" name="file" id="file" required accept="image/png, image/jpeg, image/jpg"
|
| 29 |
+
class="block w-full text-sm text-gray-500 rounded-md border border-gray-300 cursor-pointer
|
| 30 |
+
file:mr-4 file:py-2 file:px-4
|
| 31 |
+
file:rounded-md file:border-0
|
| 32 |
+
file:text-sm file:font-semibold
|
| 33 |
+
file:bg-indigo-50 file:text-indigo-700
|
| 34 |
+
hover:file:bg-indigo-100 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
|
| 35 |
+
<p class="text-xs text-gray-500 mt-1">Accepted formats: PNG, JPG, JPEG.</p>
|
| 36 |
+
</div>
|
| 37 |
+
|
| 38 |
+
<div>
|
| 39 |
+
<button type="submit"
|
| 40 |
+
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition duration-150 ease-in-out">
|
| 41 |
+
Upload and Detect
|
| 42 |
+
</button>
|
| 43 |
+
</div>
|
| 44 |
+
</form>
|
| 45 |
+
|
| 46 |
+
<!-- We can add messages here later (e.g., upload errors) -->
|
| 47 |
+
{% if error_message %}
|
| 48 |
+
<p class="mt-4 text-center text-red-600">{{ error_message }}</p>
|
| 49 |
+
{% endif %}
|
| 50 |
+
</div>
|
| 51 |
+
</body>
|
| 52 |
+
|
| 53 |
+
</html>
|
templates/result.html
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>ANPR Result</title>
|
| 8 |
+
<script src="https://www.google.com/search?q=https://cdn.tailwindcss.com"></script>
|
| 9 |
+
<link href="https://www.google.com/search?q=https://fonts.googleapis.com/css2%3Ffamily%3DInter:wght%40400%3B500%3B600%3B700%26display%3Dswap" rel="stylesheet">
|
| 10 |
+
<style>
|
| 11 |
+
body {
|
| 12 |
+
font-family: 'Inter', sans-serif;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
.result-image {
|
| 16 |
+
max-width: 100%;
|
| 17 |
+
height: auto;
|
| 18 |
+
max-height: 70vh;
|
| 19 |
+
margin: 10px auto;
|
| 20 |
+
display: block;
|
| 21 |
+
}
|
| 22 |
+
</style>
|
| 23 |
+
</head>
|
| 24 |
+
|
| 25 |
+
<body class="bg-gray-100 p-8">
|
| 26 |
+
<div class="max-w-4xl mx-auto bg-white p-6 rounded-lg shadow-md">
|
| 27 |
+
<h1 class="text-2xl font-semibold mb-6 text-center text-gray-700">ANPR Processing Result</h1>
|
| 28 |
+
|
| 29 |
+
{% if error_message %}
|
| 30 |
+
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4" role="alert">
|
| 31 |
+
<strong class="font-bold">Error:</strong>
|
| 32 |
+
<span class="block sm:inline">{{ error_message }}</span>
|
| 33 |
+
</div>
|
| 34 |
+
{% else %}
|
| 35 |
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
| 36 |
+
<div>
|
| 37 |
+
<h2 class="text-xl font-medium mb-2 text-gray-600">Original Image</h2>
|
| 38 |
+
{% if original_image_url %}
|
| 39 |
+
<img src="{{ original_image_url }}" alt="Original Uploaded Image" class="result-image rounded-md border border-gray-300">
|
| 40 |
+
{% else %}
|
| 41 |
+
<p class="text-gray-500">Original image not available.</p>
|
| 42 |
+
{% endif %}
|
| 43 |
+
</div>
|
| 44 |
+
<div>
|
| 45 |
+
<h2 class="text-xl font-medium mb-2 text-gray-600">Processed Image</h2>
|
| 46 |
+
{% if result_image_url %}
|
| 47 |
+
<img src="{{ result_image_url }}" alt="Processed Image with Detections" class="result-image rounded-md border border-gray-300">
|
| 48 |
+
{% else %}
|
| 49 |
+
<p class="text-gray-500">Processed image not available.</p>
|
| 50 |
+
{% endif %}
|
| 51 |
+
</div>
|
| 52 |
+
</div>
|
| 53 |
+
|
| 54 |
+
<div class="mb-6">
|
| 55 |
+
<h2 class="text-xl font-medium mb-2 text-gray-600">Detected Plate Texts:</h2>
|
| 56 |
+
{% if detected_texts %}
|
| 57 |
+
<ul class="list-disc list-inside bg-gray-50 p-4 rounded-md border border-gray-200">
|
| 58 |
+
{% for text in detected_texts %}
|
| 59 |
+
<li class="text-lg text-gray-800 mb-1 font-mono">{{ text }}</li>
|
| 60 |
+
{% endfor %}
|
| 61 |
+
</ul>
|
| 62 |
+
{% else %}
|
| 63 |
+
<p class="text-gray-500 bg-gray-50 p-4 rounded-md border border-gray-200">No license plates detected or text recognized.</p>
|
| 64 |
+
{% endif %}
|
| 65 |
+
</div>
|
| 66 |
+
{% endif %}
|
| 67 |
+
|
| 68 |
+
<div class="text-center mt-6">
|
| 69 |
+
<a href="{{ url_for('upload_file') }}" class="inline-block bg-blue-500 hover:bg-blue-600 text-white font-semibold py-2 px-4 rounded-md transition duration-300">Upload Another Image</a>
|
| 70 |
+
</div>
|
| 71 |
+
</div>
|
| 72 |
+
</body>
|
| 73 |
+
|
| 74 |
+
</html>
|
test.jpeg
ADDED
|
test2.jpeg
ADDED
|
yolov8n.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:31e20dde3def09e2cf938c7be6fe23d9150bbbe503982af13345706515f2ef95
|
| 3 |
+
size 6534387
|