"Optimize ALPR: async OCR, better FPS, remove CPU limits"
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user