"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
if re.match(r'^[A-Z]{4}\d{2}$', text) or re.match(r'^[A-Z]{2}\d{4}$', text):
send_plate(text)
return True
return False
print("Initializing EasyOCR...") def ocr_worker(reader):
reader = easyocr.Reader(['en'], gpu=False) # EasyOCR es pesado en CPU """Hilo dedicado para OCR - no bloquea el stream"""
while True:
try:
plate_img = ocr_queue.get(timeout=1)
if plate_img is None:
continue
print(f"Loading YOLO model...") # Preprocesamiento para mejor OCR
try: gray = cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY)
model = YOLO(MODEL_PATH)
except Exception as e: ocr_results = reader.readtext(gray, detail=0, paragraph=False,
print(f"Critical Error: {e}") allowlist='ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
return for text in ocr_results:
clean_text = ''.join(e for e in text if e.isalnum()).upper()
if len(clean_text) >= 6:
validate_and_send(clean_text)
except:
pass
def camera_loop():
"""Hilo principal de captura - mantiene FPS alto"""
global outputFrame, latest_detections
print("🚀 Initializing ALPR System...")
print("📷 Loading camera...")
cap = cv2.VideoCapture(CAMERA_ID) cap = cv2.VideoCapture(CAMERA_ID)
# Configuración de cámara - usar resolución soportada cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*'MJPG'))
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_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
cap.set(cv2.CAP_PROP_FPS, 30) cap.set(cv2.CAP_PROP_FPS, 30)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # Mantener el buffer al mínimo 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 last_process_time = 0
frame_count = 0
while True: while True:
# OPTIMIZACIÓN 2: Vaciar el buffer de la cámara # Captura eficiente - solo 2 grabs
# Leemos varios cuadros pero solo nos quedamos con el último cap.grab()
for _ in range(4): cap.grab()
cap.grab()
ret, frame = cap.retrieve() ret, frame = cap.retrieve()
if not ret: if not ret:
time.sleep(0.01)
continue continue
frame_count += 1
current_time = time.time() current_time = time.time()
# Procesamiento ALPR # Procesar ALPR cada PROCESS_INTERVAL segundos
if current_time - last_process_time > PROCESS_INTERVAL: if current_time - last_process_time > PROCESS_INTERVAL:
last_process_time = current_time last_process_time = current_time
# Ejecutar YOLO (verbose=False para no saturar la terminal) # YOLO detection - usar imgsz pequeño para velocidad
results = model(frame, verbose=False, imgsz=256) # imgsz=320 acelera mucho results = model(frame, verbose=False, imgsz=320, conf=0.5)
detections = [] new_detections = []
for r in results: for r in results:
for box in r.boxes: for box in r.boxes:
x1, y1, x2, y2 = map(int, box.xyxy[0]) x1, y1, x2, y2 = map(int, box.xyxy[0])
conf = float(box.conf[0]) conf = float(box.conf[0])
new_detections.append((x1, y1, x2, y2, conf))
if conf > 0.5: # Extraer imagen de placa y enviar a cola OCR
detections.append((x1, y1, x2, y2, conf)) plate_img = frame[y1:y2, x1:x2].copy()
plate_img = frame[y1:y2, x1:x2] if plate_img.size > 0 and not ocr_queue.full():
ocr_queue.put(plate_img)
# OCR es la parte más lenta with detection_lock:
try: latest_detections = new_detections
# 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:
clean_text = ''.join(e for e in text if e.isalnum()).upper()
validate_and_send(clean_text)
except:
pass
with lock: # Actualizar frame para streaming (sin bloquear)
latest_detections = detections display_frame = frame
with detection_lock:
# Dibujar resultados para el stream
display_frame = frame.copy()
with 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)