267 lines
9.6 KiB
Python
267 lines
9.6 KiB
Python
import os
|
|
import cv2
|
|
from pyzbar import pyzbar
|
|
import time
|
|
import logging
|
|
import sys
|
|
import yaml
|
|
import requests
|
|
from pathlib import Path
|
|
from typing import Optional, Tuple, List
|
|
from dataclasses import dataclass
|
|
from contextlib import contextmanager
|
|
import threading
|
|
from queue import Queue
|
|
from db_handler import DBHandler
|
|
|
|
@dataclass
|
|
class QRData:
|
|
"""Clase para almacenar información del código QR"""
|
|
data: str
|
|
code_type: str
|
|
coordinates: Tuple[int, int, int, int]
|
|
timestamp: float
|
|
|
|
class QRDetector:
|
|
def __init__(self, config_path: str = 'config.yaml'):
|
|
self.config = self.load_config(config_path)
|
|
self.setup_logging()
|
|
self.camera_id = self.config['camera']['id']
|
|
self.camera_name = self.config.get('camera', {}).get('name', 'NO_CAMERA_NAME')
|
|
self.min_delay = self.config['camera']['min_delay']
|
|
self.camera = None
|
|
self.last_code = None
|
|
self.last_time = 0
|
|
self.lotes = []
|
|
self.api_queue = Queue()
|
|
self.running = True
|
|
|
|
# Inicializar base de datos
|
|
self.db = DBHandler(self.config.get('database', {}).get('path', 'qr_codes.db'))
|
|
|
|
# Iniciar thread para procesar envíos a la API
|
|
self.api_thread = threading.Thread(target=self.api_worker)
|
|
self.api_thread.daemon = True
|
|
self.api_thread.start()
|
|
|
|
self.headless = self.config.get('headless', {}).get('enabled', False)
|
|
|
|
|
|
|
|
# Crear directorio para imágenes si es necesario
|
|
|
|
if self.headless and self.config['headless'].get('save_images', False):
|
|
|
|
os.makedirs(self.config['headless']['image_output_dir'], exist_ok=True)
|
|
|
|
# Verificar configuración de API
|
|
if 'api' in self.config:
|
|
self.logger.info(f"Configuración API cargada:")
|
|
self.logger.info(f"URL: {self.config['api']['url']}")
|
|
self.logger.info(f"Batch size: {self.config['api'].get('batch_size', 1)}")
|
|
else:
|
|
self.logger.error("No se encontró configuración de API")
|
|
|
|
def load_config(self, config_path: str) -> dict:
|
|
"""Carga la configuración desde el archivo YAML"""
|
|
try:
|
|
with open(config_path, 'r') as file:
|
|
return yaml.safe_load(file)
|
|
except Exception as e:
|
|
self.logger.error(f"Error cargando configuración: {e}")
|
|
sys.exit(1)
|
|
|
|
def setup_logging(self):
|
|
"""Configura el sistema de logging"""
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(levelname)s - %(message)s',
|
|
handlers=[
|
|
logging.StreamHandler(sys.stdout),
|
|
logging.FileHandler('qr_detector.log')
|
|
]
|
|
)
|
|
self.logger = logging.getLogger(__name__)
|
|
|
|
def api_worker(self):
|
|
"""Worker thread para procesar envíos a la API"""
|
|
while self.running:
|
|
try:
|
|
lotes = self.api_queue.get(timeout=1)
|
|
if lotes:
|
|
self._send_to_api(lotes)
|
|
self.api_queue.task_done()
|
|
except:
|
|
continue
|
|
|
|
def _send_to_api(self, lotes):
|
|
"""Envía los lotes a la API (método interno)"""
|
|
try:
|
|
api_url = self.config['api']['url']
|
|
headers = {
|
|
'Content-Type': 'application/json',
|
|
'x-api-key': self.config['api']['api_key']
|
|
}
|
|
|
|
payload = {
|
|
"lotes": lotes
|
|
}
|
|
|
|
self.logger.info(f"Enviando lotes a API: {lotes}")
|
|
response = requests.post(api_url, json=payload, headers=headers, timeout=5)
|
|
|
|
if response.status_code == 200:
|
|
self.logger.info(f"Lotes enviados exitosamente")
|
|
else:
|
|
self.logger.error(f"Error enviando lotes: {response.status_code}")
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error en envío a API: {e}")
|
|
|
|
def send_to_api(self):
|
|
"""Encola los lotes para envío asíncrono"""
|
|
if not self.lotes:
|
|
return
|
|
|
|
# Copiar los lotes actuales y limpiar la lista
|
|
lotes_to_send = self.lotes.copy()
|
|
self.lotes = []
|
|
|
|
# Encolar para envío asíncrono
|
|
self.api_queue.put(lotes_to_send)
|
|
|
|
@contextmanager
|
|
def initialize_camera(self):
|
|
"""Inicializa la cámara"""
|
|
try:
|
|
self.camera = cv2.VideoCapture(self.camera_id)
|
|
if not self.camera.isOpened():
|
|
self.logger.error("No se pudo abrir la cámara")
|
|
sys.exit(1)
|
|
yield self.camera
|
|
finally:
|
|
if self.camera:
|
|
self.camera.release()
|
|
cv2.destroyAllWindows()
|
|
self.send_to_api() # Enviar lotes pendientes al cerrar
|
|
self.logger.info("Recursos liberados")
|
|
|
|
def process_frame(self, frame) -> List[QRData]:
|
|
"""Procesa un frame y retorna los códigos QR detectados"""
|
|
try:
|
|
qr_codes = pyzbar.decode(frame)
|
|
current_time = time.time()
|
|
results = []
|
|
|
|
for qr_code in qr_codes:
|
|
try:
|
|
data = qr_code.data.decode('utf-8')
|
|
code_type = qr_code.type
|
|
coordinates = qr_code.rect
|
|
|
|
if (data != self.last_code or
|
|
(current_time - self.last_time) > self.min_delay):
|
|
|
|
# Verificar si el QR ya existe en la base de datos local
|
|
if self.db.check_qr_exists(data):
|
|
self.logger.info(f"QR ya existe en la base de datos: {data}")
|
|
continue
|
|
|
|
# Guardar en la base de datos local
|
|
if self.db.save_qr(data,self.camera_name):
|
|
self.logger.info(f"QR guardado en base de datos: {data}")
|
|
|
|
qr_data = QRData(
|
|
data=data,
|
|
code_type=code_type,
|
|
coordinates=coordinates,
|
|
timestamp=current_time
|
|
)
|
|
results.append(qr_data)
|
|
|
|
# Añadir a lotes para envío a API solo si es nuevo
|
|
if data not in self.lotes:
|
|
self.lotes.append(data)
|
|
self.logger.info(f"Añadido a lotes para API. Total: {len(self.lotes)}")
|
|
|
|
self.last_code = data
|
|
self.last_time = current_time
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error procesando QR individual: {e}")
|
|
|
|
return results
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error procesando frame: {e}")
|
|
return []
|
|
|
|
def draw_qr_info(self, frame, qr_data: QRData):
|
|
"""Dibuja la información del QR en el frame"""
|
|
try:
|
|
x, y, w, h = qr_data.coordinates
|
|
|
|
# Dibujar rectángulo
|
|
cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
|
|
|
|
# Mostrar información
|
|
display_text = f"{qr_data.code_type}: {qr_data.data}"
|
|
cv2.putText(
|
|
frame,
|
|
display_text,
|
|
(x, y - 10),
|
|
cv2.FONT_HERSHEY_SIMPLEX,
|
|
0.5,
|
|
(0, 255, 0),
|
|
2
|
|
)
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error dibujando información: {e}")
|
|
|
|
def run(self):
|
|
"""Método principal para ejecutar el detector"""
|
|
self.logger.info("Iniciando detector de códigos QR...")
|
|
batch_size = self.config['api'].get('batch_size', 1)
|
|
self.logger.info(f"Batch size configurado: {batch_size}")
|
|
|
|
with self.initialize_camera() as camera:
|
|
while True:
|
|
try:
|
|
success, frame = camera.read()
|
|
if not success:
|
|
self.logger.error("Error capturando frame")
|
|
break
|
|
|
|
qr_codes = self.process_frame(frame)
|
|
|
|
for qr_data in qr_codes:
|
|
if self.headless:
|
|
self.save_frame_with_qr(frame, qr_data)
|
|
else:
|
|
self.draw_qr_info(frame, qr_data)
|
|
cv2.imshow('QR Detector', frame)
|
|
|
|
if not self.headless:
|
|
if cv2.waitKey(1) & 0xFF == ord('q'):
|
|
self.running = False
|
|
if self.lotes:
|
|
self.logger.info(f"Enviando {len(self.lotes)} lotes pendientes...")
|
|
self.send_to_api()
|
|
self.api_queue.join()
|
|
break
|
|
|
|
# Verificar y enviar lotes
|
|
if len(self.lotes) >= batch_size:
|
|
self.send_to_api()
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error en el bucle principal: {e}")
|
|
continue
|
|
|
|
def main():
|
|
detector = QRDetector()
|
|
detector.run()
|
|
|
|
if __name__ == "__main__":
|
|
main() |