Sistema de Detección de Malezas en Soja con YOLOv12 e Interfaz Gráfica Avanzada

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()

Etiquetas: YOLOv12 DeepLearning ObjectDetection PyTorch Python

Publicado el 7-31 21:14