diff --git a/alpr-service/main.py b/alpr-service/main.py index bac9bba..a130704 100644 --- a/alpr-service/main.py +++ b/alpr-service/main.py @@ -4,126 +4,166 @@ import requests import os import time import threading -import numpy as np import re +from queue import Queue from flask import Flask, Response from flask_cors import CORS from ultralytics import YOLO # Configuration BACKEND_URL = os.environ.get('BACKEND_URL', 'http://localhost:3000') -CAMERA_ID = 0 -# Bajamos un poco el intervalo para ser más reactivos -PROCESS_INTERVAL = 2.0 -CONFIDENCE_THRESHOLD = 0.4 +CAMERA_ID = 0 +PROCESS_INTERVAL = 1.5 # Más reactivo MODEL_PATH = 'best.pt' app = Flask(__name__) CORS(app) +# Shared state outputFrame = None -lock = threading.Lock() -latest_detections = [] +frame_lock = threading.Lock() +latest_detections = [] +detection_lock = threading.Lock() + +# Cola para procesamiento OCR asíncrono +ocr_queue = Queue(maxsize=5) def send_plate(plate_number): + """Envía la patente detectada al backend""" try: url = f"{BACKEND_URL}/api/detect" - payload = {'plate_number': plate_number} - requests.post(url, json=payload, timeout=3) + requests.post(url, json={'plate_number': plate_number}, timeout=3) + print(f"✓ Plate sent: {plate_number}") except Exception as e: - print(f"Error sending plate: {e}") + print(f"✗ Error sending plate: {e}") -def alpr_loop(): - global outputFrame, lock, latest_detections - - print("Initializing EasyOCR...") - reader = easyocr.Reader(['en'], gpu=False) # EasyOCR es pesado en CPU - - print(f"Loading YOLO model...") - try: - model = YOLO(MODEL_PATH) - except Exception as e: - print(f"Critical Error: {e}") - return +def validate_and_send(text): + """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 +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) - # 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_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) # 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 + frame_count = 0 while True: - # OPTIMIZACIÓN 2: Vaciar el buffer de la cámara - # Leemos varios cuadros pero solo nos quedamos con el último - for _ in range(4): - cap.grab() - + # 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() - # Procesamiento ALPR + # Procesar ALPR cada PROCESS_INTERVAL segundos if current_time - last_process_time > PROCESS_INTERVAL: 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 + # YOLO detection - usar imgsz pequeño para velocidad + results = model(frame, verbose=False, imgsz=320, conf=0.5) - detections = [] + 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)) - 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: - clean_text = ''.join(e for e in text if e.isalnum()).upper() - validate_and_send(clean_text) - except: - pass - - with lock: - latest_detections = detections - - # Dibujar resultados para el stream - display_frame = frame.copy() - with lock: + # 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: 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 - 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(): - global outputFrame, lock + """Generador para streaming MJPEG""" + global outputFrame while True: - time.sleep(0.05) - with lock: - if outputFrame is None: continue - (flag, encodedImage) = cv2.imencode(".jpg", outputFrame, [cv2.IMWRITE_JPEG_QUALITY, 70]) - yield(b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + bytearray(encodedImage) + b'\r\n') + time.sleep(0.033) # ~30 FPS para el stream + with frame_lock: + if outputFrame is None: + continue + _, 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") def video_feed(): return Response(generate(), mimetype="multipart/x-mixed-replace; boundary=frame") +@app.route("/health") +def health(): + return {"status": "ok", "service": "alpr"} + if __name__ == "__main__": - t = threading.Thread(target=alpr_loop, daemon=True) + t = threading.Thread(target=camera_loop, daemon=True) t.start() app.run(host="0.0.0.0", port=5001, debug=False, threaded=True, use_reloader=False)