Introducción al Sistema de Detección
Este artículo presenta el diseño e implementación de un sistema de detección de malezas en campos de soja, utilizando el modelo de aprendizaje profundo YOLOv12. La solución combina un conjunto de datos anotado en formato YOLO con una interfaz de usuario intuitiva (UI) para ofrecer una herramienta automatizada y precisa en la gestión de malezas en la agricultura de precisión.
La capacidad de identificar eficazmente las malezas en los cultivos es crucial para optimizar el uso de herbicidas y maximizar el rendimiento. Los métodos manuales son ineficientes y costosos, lo que impulsa la adopción de modelos de detección de objetos basados en aprendizaje profundo, como la serie YOLO, conocidos por su velocidad y precisión. Sin embargo, la detección de malezas pequeñas en entornos agrícolas complejos sigue siendo un desafío.
Nuestra propuesta se basa en una versión optimizada del modelo YOLOv12, específicamente ajustada para escenarios en campos de soja. Se ha creado un conjunto de datos exclusivo con 1,302 imágenes anotadas (908 para entrenamiento, 260 para validación y 134 para prueba), clasificando objetos como 'soy plant' (planta de soja) y 'weed' (maleza). El sistema está desarrollado utilizando el framework PyTorch y la pila tecnológica de Python, e incorpora una interfaz de usuario con autenticación (registro/inicio de sesión) y visualización de detección, proporcionando un flujo de trabajo completo para la identificación de malezas.
Características Funcionales del Proyecto
El sistema ofrece un conjunto robusto de funcionalidades diseñadas para la detección eficiente y una experiencia de usuario optimizada:
- Gestión de Usuarios: Proporciona un módulo de inicio de sesión y registro con verificación de contraseña y almacenamiento local de cuentas (
accounts.json), exigiendo contraseñas de al menos 6 caracteres. - Modos de Detección Versátiles: Empleando el modelo YOLOv12, el sistema soporta tres modos de detección: análisis de imágenes, procesamiento de videos y detección en tiempo real a través de cámaras.
- Visualización Dual: Muestra simultáneamente el material original y los resultados de la detección para una comparación directa.
- Análisis de Datos Detallado: Presenta en una tabla en tiempo real la categoría, el nivel de confianza y las coordenadas de los objetos detectados.
- Ajuste de Parámetros Inteligente: Incluye un control deslizante para ajustar dinámicamente el umbral de confianza, permitiendo optimizar la precisión de la detección según las necesidades del escenario.
- Interfaz de Usuario Moderna: Con un diseño oscuro y efectos de iluminación dinámicos, minimiza la fatiga visual y mejora la interacción.
- Arquitectura Multihilo: Las tareas de detección se ejecutan en hilos separados, asegurando un rendimiento fluido, respuestas rápidas y la ausencia de interrupciones en la interfaz.
Gestión de Archivos y Configuración
- Detección de Imágenes: Compatible con formatos JPG, JPEG, PNG y BMP.
- Detección de Video: Procesamiento fotograma a fotograma para videos MP4, AVI y MOV.
- Detección por Cámara: Análisis de flujo en tiempo real desde la cámara predeterminada (cámara 0).
- Guardado de Resultados: Los resultados de la detección se almacenan automáticamente en el directorio "results".
Ajuste de Parámetros y Visualización
- Selección de Modelo: Permite elegir entre diferentes variantes del modelo YOLOv12.
- Umbrales Configurables: Ajuste del umbral de confianza (0-1.0) y del umbral IoU (Intersection over Union) (0-1.0), con sincronización en tiempo real entre los deslizadores y los campos de entrada numérica.
- Interfaz Flexible: Diseño de ventana sin bordes, redimensionable y arrastrable.
Conjunto de Datos para Detección de Malezas
Este sistema utiliza un conjunto de datos de detección de malezas en soja creado específicamente, que comprende 1,302 imágenes de alta calidad. Estas imágenes incluyen anotaciones para 'soy plant' (plantas de soja) y 'weed' (malezas) en diversas etapas de crecimiento. El conjunto de datos está estructurado para tareas estándar de detección de objetos, con la siguiente distribución:
- Conjunto de Entrenamiento: 908 imágenes, utilizadas para optimizar los parámetros del modelo y el aprendizaje de características.
- Conjunto de Validación: 260 imágenes, empleadas para el ajuste de hiperparámetros y la evaluación del rendimiento del modelo.
- Conjunto de Prueba: 134 imágenes, destinadas a la evaluación final de la capacidad de generalización del modelo, aseugrando su robustez en escenarios reales.
Configuración del Conjunto de Datos (YOLO Format)
El conjnuto de datos se organiza en el formato estándar YOLO, definido en un archivo data.yaml:
train: /ruta/a/su/conjunto/de/datos/train/images
val: /ruta/a/su/conjunto/de/datos/valid/images
test: /ruta/a/su/conjunto/de/datos/test/images
nc: 2
names: ['soy plant', 'weed']
Configuración del Entorno de Desarrollo
Para garantizar la compatibilidad y evitar conflictos de dependencias, se recomienda crear un entorno virtual para el proyecto.
Creación del Entorno Virtual
Se utiliza Anaconda para gestionar el entorno. Abra una terminal y ejecute:
conda create -n yolov12_env python==3.9
Activación del Entorno
Active el entorno recién creado:
conda activate yolov12_env
Instalación de PyTorch (Versión CPU)
Instale PyTorch sin soporte para GPU si no dispone de una, o prefiere la versión de CPU:
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
Instalación de Librerías Adicionales
Instale las demás dependencias necesarias desde el archivo requirements.txt:
pip install -r requirements.txt
Entrenamiento del Modelo
El proceso de entrenamiento del modelo YOLOv12 se realiza utilizando la API de ultralytics.
Script de Entrenamiento
A continuación, se muestra un ejemplo básico de cómo se configura y ejecuta el entrenamiento:
from ultralytics import YOLO
# Rutas a los archivos del modelo y configuración del dataset
ruta_modelo_base = 'yolov12s.pt' # Modelo pre-entrenado
ruta_config_datos = 'data.yaml' # Archivo de configuración del dataset
if __name__ == '__main__':
# Cargar el modelo YOLOv12
modelo_detector = YOLO(ruta_modelo_base)
# Iniciar el entrenamiento del modelo
resultados_entrenamiento = modelo_detector.train(
data=ruta_config_datos, # Ruta al archivo de configuración del dataset
epochs=100, # Número de épocas para el entrenamiento
batch=8, # Tamaño del lote (batch size)
device='0', # Dispositivo a usar para el entrenamiento (ej. '0' para GPU, 'cpu' para CPU)
workers=0, # Número de workers para la carga de datos (0 para el hilo principal)
project='ejecuciones', # Nombre del proyecto para guardar los resultados
name='exp_soja_malezas', # Nombre específico para esta ejecución de entrenamiento
)
Es importante seleccionar el modelo base adecuado según los requisitos de rendimiento y precisión:
yolov12n.yaml(nano): Modelo ligero, ideal para dispositivos embebidos; rápido pero con menor precisión.yolov12s.yaml(small): Modelo pequeño, adecuado para tareas en tiempo real.yolov12m.yaml(medium): Modelo de tamaño medio, que equilibra velocidad y precisión.yolov12b.yaml(base): Versión estándar, apta para la mayoría de las aplicaciones.yolov12l.yaml(large): Modelo grande, diseñado para tareas que requieren alta precisión.
Componentes Clave del Código
Módulo de Detección Multihilo
La lógica de detección se encapsula en un hilo separado para mantener la interfaz de usuario responsiva. Este hilo gestiona la carga de la fuente (imagen, video o cámara), la ejecución del modelo YOLO y la emisión de los resultados procesados.
import cv2
import numpy as np
import time
import os
from PyQt5.QtCore import QThread, pyqtSignal
from ultralytics import YOLO
class HiloDeteccion(QThread):
# Señales para comunicar resultados a la interfaz principal
cuadroRecibido = pyqtSignal(np.ndarray, np.ndarray, list) # (Cuadro original, cuadro anotado, lista de detecciones)
senialFinalizada = pyqtSignal() # Señal de finalización del hilo
def __init__(self, modelo, fuente, confianza, iou_umbral, parent=None):
super().__init__(parent)
self.modelo = modelo
self.fuente = fuente
self.confianza = confianza
self.iou_umbral = iou_umbral
self.ejecutando = True
def run(self):
try:
# Procesamiento de video o cámara
if isinstance(self.fuente, int) or str(self.fuente).lower().endswith(('.mp4', '.avi', '.mov')):
capturador = cv2.VideoCapture(self.fuente)
while self.ejecutando and capturador.isOpened():
ret, cuadro = capturador.read()
if not ret:
break
cuadro_original = cuadro.copy() # Copia del cuadro original
resultados = self.modelo(cuadro, conf=self.confianza, iou=self.iou_umbral)
cuadro_anotado = resultados[0].plot() # Anotar el cuadro con las detecciones
detecciones = []
for res in resultados:
for caja in res.boxes:
id_clase = int(caja.cls)
nombre_clase = self.modelo.names[id_clase]
confianza_obj = float(caja.conf)
x, y, w, h = caja.xywh[0].tolist() # Coordenadas y dimensiones
detecciones.append((nombre_clase, confianza_obj, x, y))
# Emitir los cuadros y detecciones
self.cuadroRecibido.emit(
cv2.cvtColor(cuadro_original, cv2.COLOR_BGR2RGB),
cv2.cvtColor(cuadro_anotado, cv2.COLOR_BGR2RGB),
detecciones
)
time.sleep(0.03) # Control de la tasa de fotogramas (aprox. 30 fps)
capturador.release()
# Procesamiento de imágenes
else:
cuadro = cv2.imread(self.fuente)
if cuadro is not None:
cuadro_original = cuadro.copy()
resultados = self.modelo(cuadro, conf=self.confianza, iou=self.iou_umbral)
cuadro_anotado = resultados[0].plot()
detecciones = []
for res in resultados:
for caja in res.boxes:
id_clase = int(caja.cls)
nombre_clase = self.modelo.names[id_clase]
confianza_obj = float(caja.conf)
x, y, w, h = caja.xywh[0].tolist()
detecciones.append((nombre_clase, confianza_obj, x, y))
self.cuadroRecibido.emit(
cv2.cvtColor(cuadro_original, cv2.COLOR_BGR2RGB),
cv2.cvtColor(cuadro_anotado, cv2.COLOR_BGR2RGB),
detecciones
)
except Exception as e:
print(f"Error en el hilo de detección: {e}")
finally:
self.senialFinalizada.emit()
def detener(self):
self.ejecutando = False
Lógica Principal de la Aplicación y Gestión de UI
La clase principal de la ventana maneja la inicialización del modelo, la interacción del usuario con la interfaz y la gestión de los hilos de detección.
from PyQt5.QtWidgets import QApplication, QMessageBox, QFileDialog, QDialog, QTableWidgetItem
from PyQt5.QtGui import QImage, QPixmap
from PyQt5.QtCore import Qt
from UiMain import UiMainWindow # Se asume que UiMain contiene la definición de la interfaz
from LoginWindow import LoginWindow # Se asume que LoginWindow contiene la ventana de login
class VentanaPrincipal(UiMainWindow):
def __init__(self):
super().__init__()
self.modelo_yolo = None
self.hilo_deteccion = None
self.imagen_actual = None
self.video_escritor = None
self.camara_activa = False
self.video_activo = False
# Conectar señales de botones a sus métodos
self.boton_imagen.clicked.connect(self.detectar_imagen)
self.boton_video.clicked.connect(self.detectar_video)
self.boton_camara.clicked.connect(self.detectar_camara)
self.boton_detener.clicked.connect(self.detener_deteccion)
self.boton_guardar.clicked.connect(self.guardar_resultado)
self.cargar_modelo_inicial()
def cargar_modelo_inicial(self):
try:
nombre_modelo = self.combo_modelo.currentText()
self.modelo_yolo = YOLO(f"{nombre_modelo}.pt")
self.actualizar_estado(f"Modelo {nombre_modelo} cargado exitosamente.")
except Exception as e:
QMessageBox.critical(self, "Error", f"Fallo al cargar el modelo: {str(e)}")
self.actualizar_estado("Fallo en la carga del modelo.")
def detectar_imagen(self):
if self.hilo_deteccion and self.hilo_deteccion.isRunning():
QMessageBox.warning(self, "Advertencia", "Por favor, detenga la tarea de detección actual primero.")
return
ruta_archivo, _ = QFileDialog.getOpenFileName(
self, "Seleccionar imagen", "", "Archivos de imagen (*.jpg *.jpeg *.png *.bmp)")
if ruta_archivo:
self.limpiar_resultados()
self.imagen_actual = cv2.imread(ruta_archivo)
self.imagen_actual = cv2.cvtColor(self.imagen_actual, cv2.COLOR_BGR2RGB)
self.mostrar_imagen(self.label_imagen_original, self.imagen_actual)
conf_umbral = self.spinbox_confianza.value()
iou_umbral = self.spinbox_iou.value()
self.hilo_deteccion = HiloDeteccion(self.modelo_yolo, ruta_archivo, conf_umbral, iou_umbral)
self.hilo_deteccion.cuadroRecibido.connect(self.al_cuadro_recibido)
self.hilo_deteccion.senialFinalizada.connect(self.al_deteccion_finalizada)
self.hilo_deteccion.start()
self.actualizar_estado(f"Detectando imagen: {os.path.basename(ruta_archivo)}")
def al_cuadro_recibido(self, cuadro_original, cuadro_anotado, detecciones):
# Este método se conectaría a HiloDeteccion.cuadroRecibido
self.mostrar_imagen(self.label_imagen_original, cuadro_original)
self.mostrar_imagen(self.label_resultado_deteccion, cuadro_anotado)
self.actualizar_tabla_resultados(detecciones)
def actualizar_tabla_resultados(self, detecciones):
self.tabla_resultados.setRowCount(0) # Limpiar tabla
for idx, (clase, confianza, x, y) in enumerate(detecciones):
self.tabla_resultados.insertRow(idx)
self.tabla_resultados.setItem(idx, 0, QTableWidgetItem(clase))
self.tabla_resultados.setItem(idx, 1, QTableWidgetItem(f"{confianza:.2f}"))
self.tabla_resultados.setItem(idx, 2, QTableWidgetItem(f"{x:.1f}"))
self.tabla_resultados.setItem(idx, 3, QTableWidgetItem(f"{y:.1f}"))
def mostrar_imagen(self, label_destino, imagen_np):
h, w, ch = imagen_np.shape
bytes_por_linea = ch * w
q_img = QImage(imagen_np.data, w, h, bytes_por_linea, QImage.Format_RGB888)
pixmap = QPixmap.fromImage(q_img)
label_destino.setPixmap(pixmap.scaled(label_destino.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation))
def actualizar_estado(self, mensaje):
self.barra_estado.showMessage(f"Estado: {mensaje} | Última actualización: {time.strftime('%H:%M:%S')}")
Autenticación de Usuario
El módulo de inicio de sesión y registro gestiona la verificación de credenciales y el registro de nuevos usuarios.
# Código simplificado de LoginWindow.py
import json
from PyQt5.QtWidgets import QDialog, QMessageBox
class VentanaLogin(QDialog):
def __init__(self):
super().__init__()
# ... inicialización de UI para usuario/contraseña/botones ...
self.cuentas = self.cargar_cuentas()
def cargar_cuentas(self):
if os.path.exists("accounts.json"):
with open("accounts.json", "r") as f:
return json.load(f)
return {}
def guardar_cuentas(self):
with open("accounts.json", "w") as f:
json.dump(self.cuentas, f, indent=4)
def manejar_inicio_sesion(self):
nombre_usuario = self.input_usuario.text().strip()
contrasena = self.input_contrasena.text().strip()
if not nombre_usuario or not contrasena:
QMessageBox.warning(self, "Advertencia", "El usuario y la contraseña no pueden estar vacíos.")
return
if nombre_usuario in self.cuentas and self.cuentas[nombre_usuario] == contrasena:
self.accept() # Inicio de sesión exitoso
else:
QMessageBox.critical(self, "Error", "Usuario o contraseña incorrectos.")
def manejar_registro(self):
nombre_usuario = self.input_usuario.text().strip()
contrasena = self.input_contrasena.text().strip()
if not nombre_usuario or not contrasena:
QMessageBox.warning(self, "Advertencia", "El usuario y la contraseña no pueden estar vacíos.")
return
if len(contrasena) < 6:
QMessageBox.warning(self, "Advertencia", "La contraseña debe tener al menos 6 caracteres.")
return
if nombre_usuario in self.cuentas:
QMessageBox.warning(self, "Advertencia", "El usuario ya existe.")
return
self.cuentas[nombre_usuario] = contrasena
self.guardar_cuentas()
QMessageBox.information(self, "Éxito", "Usuario registrado exitosamente.")
self.accept()