"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,126 +4,166 @@ 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
# 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
def ocr_worker(reader):
"""Hilo dedicado para OCR - no bloquea el stream"""
while True:
try:
plate_img = ocr_queue.get(timeout=1)
if plate_img is None:
continue
# Preprocesamiento para mejor OCR
gray = cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY)
ocr_results = reader.readtext(gray, detail=0, paragraph=False,
allowlist='ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
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
try: with detection_lock:
# OPTIMIZACIÓN 3: Solo leer el texto esencial latest_detections = new_detections
ocr_results = reader.readtext(plate_img, detail=0, paragraph=False, workers=0)
for text in ocr_results: # Actualizar frame para streaming (sin bloquear)
clean_text = ''.join(e for e in text if e.isalnum()).upper() display_frame = frame
validate_and_send(clean_text) with detection_lock:
except:
pass
with lock:
latest_detections = detections
# 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)
cv2.putText(display_frame, f"{conf:.0%}", (x1, y1-5),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)
with frame_lock:
outputFrame = display_frame outputFrame = display_frame
time.sleep(0.01)
def validate_and_send(text):
if re.match(r'^[A-Z]{4}\d{2}$', text) or re.match(r'^[A-Z]{2}\d{4}$', text):
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)