From 24b04f84ab53dfe69423b3a8dbc47c5ab5d3a548 Mon Sep 17 00:00:00 2001 From: raven Date: Mon, 12 Jan 2026 21:59:59 -0300 Subject: [PATCH] add snapshots & counter --- alpr-service/main.py | 78 +++++++++++++++++++-------- docker-compose.yml | 2 + frontend/src/pages/AdminDashboard.jsx | 33 ++++++++++-- frontend/vite.config.js | 4 ++ 4 files changed, 92 insertions(+), 25 deletions(-) diff --git a/alpr-service/main.py b/alpr-service/main.py index a130704..f1595b2 100644 --- a/alpr-service/main.py +++ b/alpr-service/main.py @@ -5,16 +5,18 @@ import os import time import threading import re +from datetime import datetime from queue import Queue -from flask import Flask, Response +from flask import Flask, Response, jsonify from flask_cors import CORS from ultralytics import YOLO # Configuration BACKEND_URL = os.environ.get('BACKEND_URL', 'http://localhost:3000') CAMERA_ID = 0 -PROCESS_INTERVAL = 1.5 # Más reactivo +PROCESS_INTERVAL = 1.5 MODEL_PATH = 'best.pt' +DATASET_DIR = '/app/dataset' # Carpeta para guardar capturas app = Flask(__name__) CORS(app) @@ -25,9 +27,32 @@ frame_lock = threading.Lock() latest_detections = [] detection_lock = threading.Lock() -# Cola para procesamiento OCR asíncrono +# Cola para procesamiento OCR asíncrono (ahora incluye frame completo) ocr_queue = Queue(maxsize=5) +# Crear carpeta de dataset si no existe +os.makedirs(DATASET_DIR, exist_ok=True) +print(f"📁 Dataset directory: {DATASET_DIR}") + +def save_plate_capture(plate_number, plate_img, full_frame): + """Guarda la captura de la patente para el dataset""" + try: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + + # Guardar imagen recortada de la patente + plate_filename = f"{DATASET_DIR}/{plate_number}_{timestamp}_plate.jpg" + cv2.imwrite(plate_filename, plate_img, [cv2.IMWRITE_JPEG_QUALITY, 95]) + + # Guardar frame completo con contexto + frame_filename = f"{DATASET_DIR}/{plate_number}_{timestamp}_full.jpg" + cv2.imwrite(frame_filename, full_frame, [cv2.IMWRITE_JPEG_QUALITY, 90]) + + print(f"📸 Saved to dataset: {plate_number}") + return True + except Exception as e: + print(f"❌ Error saving capture: {e}") + return False + def send_plate(plate_number): """Envía la patente detectada al backend""" try: @@ -37,22 +62,21 @@ def send_plate(plate_number): except Exception as e: print(f"✗ Error sending plate: {e}") -def validate_and_send(text): - """Valida formato chileno y envía""" +def validate_plate(text): + """Valida formato chileno""" # 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 + return bool(re.match(r'^[A-Z]{4}\d{2}$', text) or re.match(r'^[A-Z]{2}\d{4}$', text)) 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: + data = ocr_queue.get(timeout=1) + if data is None: continue + plate_img, full_frame = data + # Preprocesamiento para mejor OCR gray = cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY) @@ -60,8 +84,11 @@ def ocr_worker(reader): 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) + if len(clean_text) >= 6 and validate_plate(clean_text): + # Enviar al backend + send_plate(clean_text) + # Guardar captura para dataset + save_plate_capture(clean_text, plate_img, full_frame) except: pass @@ -96,10 +123,9 @@ def camera_loop(): print("✅ System ready!") last_process_time = 0 - frame_count = 0 while True: - # Captura eficiente - solo 2 grabs + # Captura eficiente cap.grab() cap.grab() ret, frame = cap.retrieve() @@ -108,14 +134,13 @@ def camera_loop(): 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 + # YOLO detection results = model(frame, verbose=False, imgsz=320, conf=0.5) new_detections = [] @@ -125,15 +150,16 @@ def camera_loop(): conf = float(box.conf[0]) new_detections.append((x1, y1, x2, y2, conf)) - # Extraer imagen de placa y enviar a cola OCR + # Extraer imagen de placa plate_img = frame[y1:y2, x1:x2].copy() if plate_img.size > 0 and not ocr_queue.full(): - ocr_queue.put(plate_img) + # Enviar placa Y frame completo para dataset + ocr_queue.put((plate_img, frame.copy())) with detection_lock: latest_detections = new_detections - # Actualizar frame para streaming (sin bloquear) + # Actualizar frame para streaming display_frame = frame with detection_lock: for (x1, y1, x2, y2, conf) in latest_detections: @@ -148,7 +174,7 @@ def generate(): """Generador para streaming MJPEG""" global outputFrame while True: - time.sleep(0.033) # ~30 FPS para el stream + time.sleep(0.033) with frame_lock: if outputFrame is None: continue @@ -163,6 +189,16 @@ def video_feed(): def health(): return {"status": "ok", "service": "alpr"} +@app.route("/dataset/count") +def dataset_count(): + """Endpoint para ver cuántas capturas hay en el dataset""" + try: + files = os.listdir(DATASET_DIR) + plates = len([f for f in files if f.endswith('_plate.jpg')]) + return {"plates_captured": plates, "total_files": len(files)} + except: + return {"plates_captured": 0, "total_files": 0} + if __name__ == "__main__": t = threading.Thread(target=camera_loop, daemon=True) t.start() diff --git a/docker-compose.yml b/docker-compose.yml index 4924346..f094f08 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -60,6 +60,8 @@ services: - backend restart: unless-stopped privileged: true + volumes: + - ./alpr-service/dataset:/app/dataset # Frontend Service (React) frontend: diff --git a/frontend/src/pages/AdminDashboard.jsx b/frontend/src/pages/AdminDashboard.jsx index 8f9bfbd..64e18ff 100644 --- a/frontend/src/pages/AdminDashboard.jsx +++ b/frontend/src/pages/AdminDashboard.jsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import axios from 'axios'; import io from 'socket.io-client'; -import { Users, CheckCircle, XCircle, Shield, Trash2, Camera, AlertCircle } from 'lucide-react'; +import { Users, CheckCircle, XCircle, Shield, Trash2, Camera, AlertCircle, Database } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import LanguageSelector from '../components/LanguageSelector'; @@ -26,8 +26,15 @@ function AdminDashboard({ token }) { const [searchRut, setSearchRut] = useState(''); const [searchResult, setSearchResult] = useState(null); // null | { found: true, data: ... } | { found: false } + // Dataset State + const [datasetCount, setDatasetCount] = useState(0); + useEffect(() => { fetchData(); + fetchDatasetCount(); + + // Actualizar contador de dataset cada 10 segundos + const datasetInterval = setInterval(fetchDatasetCount, 10000); // Live detection listener socket.on('new_detection', (data) => { @@ -48,6 +55,7 @@ function AdminDashboard({ token }) { socket.off('plate_status_updated'); socket.off('plate_deleted'); socket.off('person_deleted'); + clearInterval(datasetInterval); }; }, [token]); @@ -88,6 +96,15 @@ function AdminDashboard({ token }) { } }; + const fetchDatasetCount = async () => { + try { + const res = await axios.get('/dataset/count'); + setDatasetCount(res.data.plates_captured || 0); + } catch (err) { + console.log('Dataset count not available'); + } + }; + const handleSearchRut = (e) => { e.preventDefault(); const normalizedRut = searchRut.replace(/\./g, '').toUpperCase(); @@ -201,9 +218,17 @@ function AdminDashboard({ token }) {

{t('monitor_area')}

-
- - +
+ {/* Dataset Counter */} +
+ + {datasetCount} + capturas +
+
+ + +
diff --git a/frontend/vite.config.js b/frontend/vite.config.js index b6f06cb..097e7fa 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -18,6 +18,10 @@ export default defineConfig({ '/video_feed': { target: 'http://alpr-service:5001', changeOrigin: true + }, + '/dataset': { + target: 'http://alpr-service:5001', + changeOrigin: true } } }