"Optimize ALPR: async OCR, better FPS, remove CPU limits"

This commit is contained in:
2026-01-12 21:39:12 -03:00
parent b9409cc48c
commit de399fe3f8

View File

@@ -4,8 +4,8 @@ import requests
import os import os
import time import time
import threading import threading
import numpy as np
import re import re
from queue import Queue
from flask import Flask, Response from flask import Flask, Response
from flask_cors import CORS from flask_cors import CORS
from ultralytics import YOLO from ultralytics import YOLO
@@ -13,117 +13,157 @@ from ultralytics import YOLO
# Configuration # Configuration
BACKEND_URL = os.environ.get('BACKEND_URL', 'http://localhost:3000') BACKEND_URL = os.environ.get('BACKEND_URL', 'http://localhost:3000')
CAMERA_ID = 0 CAMERA_ID = 0
# Bajamos un poco el intervalo para ser más reactivos PROCESS_INTERVAL = 1.5 # Más reactivo
PROCESS_INTERVAL = 2.0
CONFIDENCE_THRESHOLD = 0.4
MODEL_PATH = 'best.pt' MODEL_PATH = 'best.pt'
app = Flask(__name__) app = Flask(__name__)
CORS(app) CORS(app)
# Shared state
outputFrame = None outputFrame = None
lock = threading.Lock() frame_lock = threading.Lock()
latest_detections = [] latest_detections = []
detection_lock = threading.Lock()
# Cola para procesamiento OCR asíncrono
ocr_queue = Queue(maxsize=5)
def send_plate(plate_number): def send_plate(plate_number):
"""Envía la patente detectada al backend"""
try: try:
url = f"{BACKEND_URL}/api/detect" url = f"{BACKEND_URL}/api/detect"
payload = {'plate_number': plate_number} requests.post(url, json={'plate_number': plate_number}, timeout=3)
requests.post(url, json=payload, timeout=3) print(f"✓ Plate sent: {plate_number}")
except Exception as e: except Exception as e:
print(f"Error sending plate: {e}") print(f"Error sending plate: {e}")
def alpr_loop(): def validate_and_send(text):
global outputFrame, lock, latest_detections """Valida formato chileno y envía"""
# Formato nuevo: XXXX-00 | Formato antiguo: XX-0000
print("Initializing EasyOCR...") if re.match(r'^[A-Z]{4}\d{2}$', text) or re.match(r'^[A-Z]{2}\d{4}$', text):
reader = easyocr.Reader(['en'], gpu=False) # EasyOCR es pesado en CPU send_plate(text)
return True
print(f"Loading YOLO model...") return False
try:
model = YOLO(MODEL_PATH)
except Exception as e:
print(f"Critical Error: {e}")
return
cap = cv2.VideoCapture(CAMERA_ID)
# Configuración de cámara - usar resolución soportada
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*'MJPG')) # Forzar MJPG
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
cap.set(cv2.CAP_PROP_FPS, 30)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # Mantener el buffer al mínimo
last_process_time = 0
def ocr_worker(reader):
"""Hilo dedicado para OCR - no bloquea el stream"""
while True: while True:
# OPTIMIZACIÓN 2: Vaciar el buffer de la cámara try:
# Leemos varios cuadros pero solo nos quedamos con el último plate_img = ocr_queue.get(timeout=1)
for _ in range(4): if plate_img is None:
cap.grab()
ret, frame = cap.retrieve()
if not ret:
continue continue
current_time = time.time() # Preprocesamiento para mejor OCR
gray = cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY)
# Procesamiento ALPR ocr_results = reader.readtext(gray, detail=0, paragraph=False,
if current_time - last_process_time > PROCESS_INTERVAL: allowlist='ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
last_process_time = current_time
# Ejecutar YOLO (verbose=False para no saturar la terminal)
results = model(frame, verbose=False, imgsz=256) # imgsz=320 acelera mucho
detections = []
for r in results:
for box in r.boxes:
x1, y1, x2, y2 = map(int, box.xyxy[0])
conf = float(box.conf[0])
if conf > 0.5:
detections.append((x1, y1, x2, y2, conf))
plate_img = frame[y1:y2, x1:x2]
# OCR es la parte más lenta
try:
# OPTIMIZACIÓN 3: Solo leer el texto esencial
ocr_results = reader.readtext(plate_img, detail=0, paragraph=False, workers=0)
for text in ocr_results: for text in ocr_results:
clean_text = ''.join(e for e in text if e.isalnum()).upper() clean_text = ''.join(e for e in text if e.isalnum()).upper()
if len(clean_text) >= 6:
validate_and_send(clean_text) validate_and_send(clean_text)
except: except:
pass pass
with lock: def camera_loop():
latest_detections = detections """Hilo principal de captura - mantiene FPS alto"""
global outputFrame, latest_detections
# Dibujar resultados para el stream print("🚀 Initializing ALPR System...")
display_frame = frame.copy() print("📷 Loading camera...")
with lock:
cap = cv2.VideoCapture(CAMERA_ID)
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*'MJPG'))
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
cap.set(cv2.CAP_PROP_FPS, 30)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
print("🧠 Loading YOLO model...")
try:
model = YOLO(MODEL_PATH)
except Exception as e:
print(f"❌ Critical Error loading model: {e}")
return
print("📝 Initializing EasyOCR...")
reader = easyocr.Reader(['en'], gpu=False)
# Iniciar worker de OCR
ocr_thread = threading.Thread(target=ocr_worker, args=(reader,), daemon=True)
ocr_thread.start()
print("✅ System ready!")
last_process_time = 0
frame_count = 0
while True:
# Captura eficiente - solo 2 grabs
cap.grab()
cap.grab()
ret, frame = cap.retrieve()
if not ret:
time.sleep(0.01)
continue
frame_count += 1
current_time = time.time()
# Procesar ALPR cada PROCESS_INTERVAL segundos
if current_time - last_process_time > PROCESS_INTERVAL:
last_process_time = current_time
# YOLO detection - usar imgsz pequeño para velocidad
results = model(frame, verbose=False, imgsz=320, conf=0.5)
new_detections = []
for r in results:
for box in r.boxes:
x1, y1, x2, y2 = map(int, box.xyxy[0])
conf = float(box.conf[0])
new_detections.append((x1, y1, x2, y2, conf))
# Extraer imagen de placa y enviar a cola OCR
plate_img = frame[y1:y2, x1:x2].copy()
if plate_img.size > 0 and not ocr_queue.full():
ocr_queue.put(plate_img)
with detection_lock:
latest_detections = new_detections
# Actualizar frame para streaming (sin bloquear)
display_frame = frame
with detection_lock:
for (x1, y1, x2, y2, conf) in latest_detections: for (x1, y1, x2, y2, conf) in latest_detections:
cv2.rectangle(display_frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.rectangle(display_frame, (x1, y1), (x2, y2), (0, 255, 0), 2)
outputFrame = display_frame cv2.putText(display_frame, f"{conf:.0%}", (x1, y1-5),
time.sleep(0.01) cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)
def validate_and_send(text): with frame_lock:
if re.match(r'^[A-Z]{4}\d{2}$', text) or re.match(r'^[A-Z]{2}\d{4}$', text): outputFrame = display_frame
send_plate(text)
def generate(): def generate():
global outputFrame, lock """Generador para streaming MJPEG"""
global outputFrame
while True: while True:
time.sleep(0.05) time.sleep(0.033) # ~30 FPS para el stream
with lock: with frame_lock:
if outputFrame is None: continue if outputFrame is None:
(flag, encodedImage) = cv2.imencode(".jpg", outputFrame, [cv2.IMWRITE_JPEG_QUALITY, 70]) continue
yield(b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + bytearray(encodedImage) + b'\r\n') _, encoded = cv2.imencode(".jpg", outputFrame, [cv2.IMWRITE_JPEG_QUALITY, 75])
yield b'--frame\r\nContent-Type: image/jpeg\r\n\r\n' + encoded.tobytes() + b'\r\n'
@app.route("/video_feed") @app.route("/video_feed")
def video_feed(): def video_feed():
return Response(generate(), mimetype="multipart/x-mixed-replace; boundary=frame") return Response(generate(), mimetype="multipart/x-mixed-replace; boundary=frame")
@app.route("/health")
def health():
return {"status": "ok", "service": "alpr"}
if __name__ == "__main__": if __name__ == "__main__":
t = threading.Thread(target=alpr_loop, daemon=True) t = threading.Thread(target=camera_loop, daemon=True)
t.start() t.start()
app.run(host="0.0.0.0", port=5001, debug=False, threaded=True, use_reloader=False) app.run(host="0.0.0.0", port=5001, debug=False, threaded=True, use_reloader=False)