Spaces:
Sleeping
Sleeping
| """ | |
| Sistema de Control de Acceso Vehicular (ANPR + Barrera) | |
| ========================================================= | |
| Aplicación standalone. Ejecutar con: | |
| python app.py | |
| Requiere que 'best.pt' (el modelo entrenado) esté en la misma carpeta que este script. | |
| Al correr, abre automáticamente una interfaz web en el navegador donde se sube un video | |
| y se obtiene: video anotado, registro de accesos, y CSV descargable. | |
| """ | |
| import os | |
| import cv2 | |
| import torch | |
| import tempfile | |
| import pandas as pd | |
| from difflib import SequenceMatcher | |
| from ultralytics import YOLO | |
| import easyocr | |
| import gradio as gr | |
| # --------------------------------------------------------------------------- | |
| # Carga de modelo y OCR | |
| # --------------------------------------------------------------------------- | |
| MODEL_PATH = 'best.pt' | |
| if not os.path.exists(MODEL_PATH): | |
| raise FileNotFoundError( | |
| "No se encontró 'best.pt' en esta carpeta. " | |
| "Colocá el archivo del modelo entrenado junto a app.py antes de ejecutar." | |
| ) | |
| GPU_DISPONIBLE = torch.cuda.is_available() | |
| print(f'GPU disponible: {GPU_DISPONIBLE}') | |
| if not GPU_DISPONIBLE: | |
| print('Corriendo en CPU. El procesamiento será más lento, pero funciona igual.') | |
| model = YOLO(MODEL_PATH) # Ultralytics usa GPU automáticamente si está disponible | |
| reader = easyocr.Reader(['en'], gpu=GPU_DISPONIBLE) | |
| print('Modelo y OCR cargados correctamente.') | |
| # --------------------------------------------------------------------------- | |
| # OCR sobre el recorte de la placa | |
| # --------------------------------------------------------------------------- | |
| def leer_placa(frame, box): | |
| x1, y1, x2, y2 = map(int, box) | |
| recorte = frame[max(0, y1):y2, max(0, x1):x2] | |
| if recorte.size == 0: | |
| return '', 0.0 | |
| resultado = reader.readtext(recorte) | |
| if not resultado: | |
| return '', 0.0 | |
| texto = ''.join([r[1] for r in resultado]).strip().upper() | |
| confianza = sum(r[2] for r in resultado) / len(resultado) | |
| return texto, confianza | |
| # --------------------------------------------------------------------------- | |
| # Lógica de control de acceso | |
| # --------------------------------------------------------------------------- | |
| def es_similar(a, b, umbral=0.75): | |
| if not a or not b: | |
| return False | |
| return SequenceMatcher(None, a, b).ratio() >= umbral | |
| def verificar_acceso(texto_leido, placas_autorizadas): | |
| for autorizada in placas_autorizadas: | |
| if es_similar(texto_leido, autorizada.strip().upper().replace(' ', '')): | |
| return True, autorizada | |
| return False, None | |
| def cajas_se_superponen(box1, box2, iou_min=0.3): | |
| x1 = max(box1[0], box2[0]) | |
| y1 = max(box1[1], box2[1]) | |
| x2 = min(box1[2], box2[2]) | |
| y2 = min(box1[3], box2[3]) | |
| inter = max(0, x2 - x1) * max(0, y2 - y1) | |
| area1 = (box1[2] - box1[0]) * (box1[3] - box1[1]) | |
| area2 = (box2[2] - box2[0]) * (box2[3] - box2[1]) | |
| union = area1 + area2 - inter | |
| iou = inter / union if union > 0 else 0.0 | |
| return iou >= iou_min | |
| def dibujar_barrera(frame, permitido, texto_placa): | |
| h, w = frame.shape[:2] | |
| color = (0, 200, 0) if permitido else (0, 0, 220) | |
| mensaje = ( | |
| f'ACCESO PERMITIDO - BARRERA ABIERTA: {texto_placa}' | |
| if permitido else | |
| f'ACCESO DENEGADO - BARRERA CERRADA: {texto_placa}' | |
| ) | |
| overlay = frame.copy() | |
| cv2.rectangle(overlay, (0, 0), (w, 50), color, -1) | |
| frame = cv2.addWeighted(overlay, 0.6, frame, 0.4, 0) | |
| cv2.putText(frame, mensaje, (10, 33), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2) | |
| return frame | |
| # --------------------------------------------------------------------------- | |
| # Procesamiento del video (detección + OCR + decisión estabilizada + registro) | |
| # --------------------------------------------------------------------------- | |
| def procesar_video(video_path, placas_autorizadas_str, confianza=0.25, | |
| ocr_cada_n_frames=20, frames_gracia_desaparicion=15, progress=gr.Progress()): | |
| placas_autorizadas = [p for p in placas_autorizadas_str.split(',') if p.strip()] | |
| cap = cv2.VideoCapture(video_path) | |
| fps = cap.get(cv2.CAP_PROP_FPS) or 25 | |
| w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) | |
| h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) | |
| total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) or 1 | |
| out_path = tempfile.NamedTemporaryFile(suffix='.mp4', delete=False).name | |
| fourcc = cv2.VideoWriter_fourcc(*'mp4v') | |
| writer = cv2.VideoWriter(out_path, fourcc, fps, (w, h)) | |
| registro = [] # (placa, timestamp, resultado) | |
| def estado_vacio(): | |
| return {'texto': '', 'permitido': False, 'activo': False, 'confianza': 0.0, | |
| 'box': None, 'decidido': False, 'frames_sin_deteccion': 0} | |
| estado_actual = estado_vacio() | |
| frame_idx = 0 | |
| progress(0, desc=f'Iniciando procesamiento ({total_frames} frames totales)...') | |
| while True: | |
| ret, frame = cap.read() | |
| if not ret: | |
| break | |
| results = model.predict(frame, conf=confianza, verbose=False)[0] | |
| timestamp = round(frame_idx / fps, 1) | |
| vehiculo_presente_este_frame = False | |
| for box in results.boxes.xyxy.cpu().numpy(): | |
| x1, y1, x2, y2 = map(int, box) | |
| es_el_que_seguimos = ( | |
| estado_actual['box'] is None or | |
| cajas_se_superponen(box, estado_actual['box']) | |
| ) | |
| if not es_el_que_seguimos: | |
| # Un vehículo distinto mientras ya se sigue a otro: se ignora hasta que el actual desaparezca. | |
| cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 255), 2) | |
| continue | |
| estado_actual['box'] = box | |
| estado_actual['frames_sin_deteccion'] = 0 | |
| vehiculo_presente_este_frame = True | |
| if not estado_actual['decidido'] and frame_idx % ocr_cada_n_frames == 0: | |
| texto, conf_lectura = leer_placa(frame, box) | |
| if texto and conf_lectura > estado_actual['confianza']: | |
| permitido, _ = verificar_acceso(texto, placas_autorizadas) | |
| es_primer_registro = not estado_actual['activo'] | |
| estado_actual.update({'texto': texto, 'permitido': permitido, 'activo': True, 'confianza': conf_lectura}) | |
| if permitido: | |
| estado_actual['decidido'] = True # ya está confirmado: se deja de leer, solo se sigue el tracker | |
| if es_primer_registro: | |
| registro.append((texto, timestamp, 'PERMITIDO' if permitido else 'DENEGADO')) | |
| elif registro: | |
| registro[-1] = (texto, registro[-1][1], 'PERMITIDO' if permitido else 'DENEGADO') | |
| color_caja = (0, 200, 0) if estado_actual['permitido'] else (0, 0, 220) | |
| cv2.rectangle(frame, (x1, y1), (x2, y2), color_caja, 2) | |
| if estado_actual['box'] is not None and not vehiculo_presente_este_frame: | |
| estado_actual['frames_sin_deteccion'] += 1 | |
| if estado_actual['frames_sin_deteccion'] >= frames_gracia_desaparicion: | |
| # El vehículo ya no está en cuadro: se libera el seguimiento para poder leer el próximo. | |
| estado_actual = estado_vacio() | |
| if estado_actual['activo']: | |
| frame = dibujar_barrera(frame, estado_actual['permitido'], estado_actual['texto']) | |
| writer.write(frame) | |
| frame_idx += 1 | |
| if frame_idx % 50 == 0 or frame_idx == total_frames - 1: | |
| progress(min(frame_idx / total_frames, 1.0), desc=f'Procesando frame {frame_idx}/{total_frames}') | |
| cap.release() | |
| writer.release() | |
| df = pd.DataFrame(registro, columns=['Placa detectada', 'Timestamp (s)', 'Resultado']) | |
| csv_path = tempfile.NamedTemporaryFile(suffix='.csv', delete=False).name | |
| df.to_csv(csv_path, index=False) | |
| n_permitidos = sum(1 for r in registro if r[2] == 'PERMITIDO') | |
| n_denegados = len(registro) - n_permitidos | |
| resumen = f'Vehículos detectados: {len(registro)} | Acceso permitido: {n_permitidos} | Acceso denegado: {n_denegados}' | |
| return out_path, df, csv_path, resumen | |
| # --------------------------------------------------------------------------- | |
| # Interfaz web (Gradio) | |
| # --------------------------------------------------------------------------- | |
| demo = gr.Interface( | |
| fn=procesar_video, | |
| inputs=[ | |
| gr.Video(label='Subí un video de vehículos'), | |
| gr.Textbox(label='Placas autorizadas (separadas por coma)', placeholder='ABC123, XYZ789', value='TS07JS9670'), | |
| gr.Slider(0.1, 0.9, value=0.25, step=0.05, label='Umbral de confianza de detección') | |
| ], | |
| outputs=[ | |
| gr.Video(label='Video con resultado de acceso'), | |
| gr.Dataframe(label='Registro de accesos'), | |
| gr.File(label='Descargar registro (CSV)'), | |
| gr.Textbox(label='Resumen') | |
| ], | |
| title='Sistema de Control de Acceso Vehicular (ANPR)', | |
| description=( | |
| 'Detección de placas (YOLOv8n) + lectura OCR + verificación contra lista de ' | |
| 'autorizados, simulando la apertura/cierre de una barrera de estacionamiento.' | |
| ) | |
| ) | |
| if __name__ == '__main__': | |
| demo.launch() |