Cuando el Deep Learning se sumerge bajo la superficie: cartografía de corales con QGIS y PyTorch

El aprendizaje profundo está revolucionando el análisis de imágenes satelitales.

Durante mucho tiempo reservado a los grandes laboratorios o al software propietario, hoy en día se abre al mundo libre gracias a PyTorch y QGIS.

Este artículo explora los principios del aprendizaje profundo aplicado a la geomática, la comparación entre los modelos de ESRI y los que se pueden utilizar en QGIS, y concluye con un caso concreto: la detección automática de zonas coralinas a partir de imágenes Sentinel-2.



Introducción al aprendizaje profundo aplicado a la geomática

El aprendizaje profundo (o deep learning) es una rama de la inteligencia artificial inspirada en el funcionamiento del cerebro humano. Se basa en redes de neuronas artificiales capaces de aprender a partir de ejemplos, sin que se les dicten explícitamente todas las reglas.

A diferencia de los métodos de clasificación tradicionales, en los que se eligen los indicadores y los umbrales, el aprendizaje profundo descubre automáticamente las estructuras y los patrones relevantes en los datos.

En el campo de la geomática, este enfoque abre perspectivas impresionantes:

  • reconocimiento de zonas urbanas, agrícolas o forestales a partir de imágenes satelitales,
  • detección de cambios o catástrofes naturales,
  • identificación de elementos específicos (carreteras, tejados, corales, barcos, etc.),
  • segmentación detallada de paisajes a partir de imágenes Sentinel, PlanetScope o drones.

El principio es sencillo: se entrena un modelo con un gran conjunto de imágenes anotadas hasta que aprende a reproducir la tarea deseada (por ejemplo, distinguir entre agua, vegetación y arena). Una vez entrenado, el modelo se puede aplicar a nuevas zonas para generar automáticamente mapas temáticos de alta resolución.


¿Por qué «profundo»?

El término «profundo» se debe a que estas redes tienen muchas capas sucesivas, a veces decenas.

Cada capa aprende a reconocer patrones cada vez más complejos:

  • las primeras capas detectan bordes y texturas,
  • las siguientes identifican formas o estructuras,
  • y las últimas comprenden objetos completos o contextos.

Es esta jerarquía de representaciones la que da al aprendizaje profundo su poder, pero también su apetito por los datos y la potencia de cálculo.


Aprendizaje profundo y teledetección

En teledetección, los modelos más utilizados son los de segmentación de imágenes, capaces de asignar una clase a cada píxel.

Arquitecturas como U-Net, DeepLab o Mask R-CNN se han convertido en referencias para la cartografía automática a partir de imágenes multiespectrales.

Estos modelos suelen entrenarse con marcos como PyTorch o TensorFlow y luego se implementan en entornos SIG.

Los dos grandes mundos se han interesado por ellos:

  • ESRI, con su formato de modelo .dlpk (Deep Learning Package) integrado en ArcGIS Pro y ArcGIS Online;
  • QGIS, que permite utilizar modelos PyTorch o TensorFlow a través de Processing Toolbox o scripts Python personalizados.


El formato DLPK de ESRI

ESRI fue uno de los primeros actores SIG en integrar el Deep Learning directamente en su ecosistema ArcGIS.

Para facilitar el intercambio y la implementación de modelos, la empresa ha creado un formato estándar: el DLPK (Deep Learning Package).

Un archivo .dlpk no es solo un modelo: es un conjunto completo y portátil que reúne todos los elementos necesarios para su ejecución.

Por lo general, contiene:

  • El modelo entrenado (a menudo en formato PyTorch .pth o TensorFlow .h5)
  • Un archivo de definición JSON que describe la arquitectura del modelo, los parámetros esperados y los nombres de las clases.
  • Metadatos: tipo de entrada (raster, mosaico, imagen), tamaño de los parches, número de bandas, normalización, etc.
  • Muestras de entrenamiento (opcionales) para documentar o volver a entrenar el modelo.

Gracias a esta organización, ArcGIS Pro o ArcGIS Online pueden interpretar automáticamente el modelo, sin necesidad de escribir código.

Herramientas como «Classify Pixels Using Deep Learning» o «Detect Objects Using Deep Learning» leen directamente el .dlpk, cargan el modelo y realizan la inferencia en una raster o un conjunto de imágenes.

Este enfoque «llave en mano» presenta dos ventajas principales:

  1. Interoperabilidad: cualquier usuario de ArcGIS puede utilizar un modelo creado en otro lugar, sin dependencias complejas.
  2. Replicabilidad: los metadatos garantizan que el modelo se aplique en las mismas condiciones que durante su entrenamiento.

La otra cara de la moneda, por supuesto, es el formato cerrado: el .dlpk sigue vinculado al ecosistema ArcGIS y no siempre es fácil de utilizar en otros entornos.


La contrapartida de código abierto: PyTorch y QGIS

En el ámbito del software libre, QGIS no impone ningún formato propietario.

Los modelos simplemente se guardan en su formato nativo (a menudo .pth para PyTorch o .pt) y se ejecutan mediante scripts Python integrados en Processing Toolbox.

La idea es la misma que en ESRI:

se carga una imagen multiespectral, se aplica un modelo entrenado y se genera un mapa de clases o de probabilidad.

Pero en lugar de basarse en un formato empaquetado como .dlpk, QGIS deja total libertad al desarrollador:

  • El modelo se lee con torch.load().
  • Las bandas de entrada se pueden seleccionar dinámicamente (por ejemplo, B4-B3-B2 para RGB, o B8-B4-B3 en falso color).
  • El script Python controla todo el flujo de procesamiento: normalización, enmascaramiento del agua (NDWI), división en bloques, fusión de resultados, etc.

Este enfoque permite una flexibilidad máxima, especialmente útil para la investigación o la experimentación.

Por ejemplo, un modelo U-Net entrenado en PyTorch se puede aplicar directamente en QGIS mediante un script personalizado, sin depender de ArcGIS Pro.

QGIS se convierte así en un auténtico laboratorio de análisis espacial con IA, donde el usuario puede:

  • probar varios modelos (.pth) creados por la comunidad,
  • adaptar los pretratamientos según la zona (franja costera, bosque, urbano…),
  • automatizar todo un flujo mediante algoritmos de procesamiento,
  • y combinar los resultados con otras capas SIG (vegetación, batimetría, ocupación del suelo…).


En resumen

Aspecto ESRI (DLPK) QGIS (PyTorch)
Formato .dlpk (paquete completo) .pth o .pt (solo modelo)
Interoperabilidad Simple, pero propietaria Libre y modificable
Uso Herramientas integradas ArcGIS («Classify Pixels», «Detect Objects») Scripts Processing Python personalizados
Personalización Limitada Total
Curva de aprendizaje Más sencilla para el usuario final Más flexible para el desarrollador o investigadorou chercheur


Ejemplo práctico: segmentación de corales a partir de imágenes Sentinel-2 en QGIS

Después de explorar la lógica de los modelos profundos y los formatos utilizados por ESRI y QGIS, pasemos a un ejemplo concreto: el análisis de zonas coralinas a partir de imágenes satelitales Sentinel-2.
El objetivo es distinguir las zonas marinas (fondos arenosos, praderas marinas, corales) de las zonas terrestres o turbias, gracias a un modelo U-Net preentrenado en PyTorch.


Datos utilizados

La imagen de entrada procede de Sentinel-2, misión del programa europeo Copernicus.
Estas imágenes multiespectrales gratuitas ofrecen una resolución de 10 a 20 m y cubren varias bandas en el espectro visible e infrarrojo:

BandaNombreLongitud de onda (nm)Uso principalB2Azul490Agua, turbidezB3Verde560Vegetación, medio marinoB4Rojo665Suelo, vegetación, coralesB8NIR842Diferenciación tierra/aguaB11SWIR11610Humedad, arena, nubes

Band Nombre Longitud de onda (nm) Uso principal
B2 Azul 490 Agua, turbidez
B3 Verde 560 Vegetación, medio marino
B4 Rojo 665 Suelo, vegetación, corales
B8 NIR 842 Diferenciación tierra/agua
B11 SWIR1 1610 Humedad, arena, nubes

La trama proviene del procesamiento S2DR3 (Super-resolution Sentinel-2 a 1 m), que permite mejorar la resolución de todas las bandas de las imágenes Sentinel 2 hasta alcanzar aproximadamente 1 m (véase «Utilizar S2DR3 en Google Colab para el estudio de los corales en Mauricio»). La imagen utilizada es la que se empleó como ejemplo en el artículo citado.
El usuario solo tiene que cargar el archivo .tif multiespectral, por ejemplo:
Palmar_MS.tif.


Procesamiento en QGIS

El análisis se realiza mediante un algoritmo Python integrado en la caja de herramientas Processing.

Este script carga un modelo PyTorch (unet_coraux.pth) y aplica la segmentación a la imagen en varios pasos:

  1. Lectura de la trama y normalización de las bandas.
    Los valores se reducen a una escala compatible con el entrenamiento del modelo.
  2. Enmascaramiento opcional de las zonas terrestres
    Se calcula un índice NDWI (Normalized Difference Water Index) para aislar el mar.
    Los píxeles con un valor NDWI alto se consideran marinos y son procesados por el modelo; los demás se enmascaran.
  3. División en bloques (parches)
    La imagen se procesa por partes para evitar la sobrecarga de memoria.
    Cada bloque se analiza de forma independiente y, a continuación, se fusionan los resultados.
  4. Aplicación del modelo U-Net
    El modelo realiza una segmentación píxel a píxel: asigna a cada píxel una probabilidad de pertenecer a la clase «coral» o «no coral».
    El resultado es una trama de salida que contiene los valores de probabilidad o de clase.
  5. Guardar la trama de salida
    El resultado se guarda en formato GeoTIFF (palmar_model_9.tif), listo para superponerse con otras capas SIG.


Modo de funcionamiento

Operaciones previas

Vamos a crear un script de procesamiento QGis. Como requisito previo, es necesario instalar

OsGeo4W shell

python -m pip install torchgeo
python -m pip install torch torchvision
python -m pip install segmentation-models-pytorch

A continuación, debemos descargar el modelo deseado. Para ello, en la consola Python de QGIS, introduzca el siguiente script:

Python Console

import segmentation_models_pytorch as smp
import torch

# U-Net RGB pré-entraîné
model = smp.Unet(
    encoder_name="resnet34",
    encoder_weights="imagenet",
    in_channels=3,
    classes=1  # corail/non-corail
)

# Sauvegarde pour QGIS
torch.save(model, "c:/models/unet_coraux.pth")

En este ejemplo, guardamos los modelos descargados en un directorio c:/models


Configuración del script de procesamiento

En QGIS → Caja de herramientas de procesamiento → Scripts → Nuevo script,

traitement

# -*- coding: utf-8 -*-
"""
Algorithme QGIS : Segmentation Coraux (U-Net) avec option masquage terrestre
Compatible QGIS 3.44+
"""
from qgis.core import (
    QgsProcessing,
    QgsProcessingAlgorithm,
    QgsProcessingParameterRasterLayer,
    QgsProcessingParameterFile,
    QgsProcessingParameterEnum,
    QgsProcessingParameterRasterDestination,
    QgsProcessingParameterBoolean,
)
from qgis.PyQt.QtCore import QCoreApplication
import torch
import torch.serialization
import numpy as np
from osgeo import gdal
from scipy.signal import windows


class SegmentationCoraux(QgsProcessingAlgorithm):
    """Segmentation des images Sentinel-2 à 1m avec un modèle U-Net"""

    BAND_OPTIONS = ["RGB (4-3-2)", "Fausse couleur (8-4-3)"]

    def initAlgorithm(self, config=None):
        self.addParameter(
            QgsProcessingParameterRasterLayer(
                "raster_input",
                self.tr("Image Sentinel-2 (GeoTIFF multibandes)")
            )
        )

        self.addParameter(
            QgsProcessingParameterEnum(
                "band_set",
                self.tr("Sélection manuelle des bandes"),
                options=self.BAND_OPTIONS,
                defaultValue=0
            )
        )

        self.addParameter(
            QgsProcessingParameterFile(
                "model_path",
                self.tr("Modèle PyTorch (.pth)"),
                extension="pth"
            )
        )

        self.addParameter(
            QgsProcessingParameterRasterDestination(
                "output_raster",
                self.tr("Raster de sortie (masque)")
            )
        )

        # --- Nouvelle option : masquage terrestre ---
        self.addParameter(
            QgsProcessingParameterBoolean(
                "mask_land",
                self.tr("Masquer les zones terrestres (NDWI)"),
                defaultValue=True
            )
        )

    def processAlgorithm(self, parameters, context, feedback):
        model_path = self.parameterAsFile(parameters, "model_path", context)
        mask_land = self.parameterAsBool(parameters, "mask_land", context)
        band_set = self.parameterAsEnum(parameters, "band_set", context)

        feedback.pushInfo("Chargement du modèle PyTorch...")
        try:
            from segmentation_models_pytorch.decoders.unet.model import Unet
            torch.serialization.add_safe_globals([Unet])
        except Exception as e:
            feedback.pushInfo(f"Avertissement : impossible d’ajouter Unet aux classes sûres : {e}")

        model = torch.load(model_path, map_location=torch.device('cpu'), weights_only=False)
        model.eval()

        raster_input = self.parameterAsRasterLayer(parameters, "raster_input", context)
        ds = gdal.Open(raster_input.source())
        feedback.pushInfo(f"Ouverture du raster : {raster_input.source()}")
        nrows, ncols = ds.RasterYSize, ds.RasterXSize

        # --- Détecter automatiquement les canaux d'entrée ---
        conv1_weights = model.encoder.conv1.weight.data
        n_channels = conv1_weights.shape[1]
        feedback.pushInfo(f"Le modèle attend {n_channels} canaux d'entrée.")

        if n_channels == 3:
            # Analyser l’importance des canaux
            channel_mean = conv1_weights.abs().mean(dim=(0, 2, 3)).numpy()
            sorted_idx = list(np.argsort(-channel_mean))
            feedback.pushInfo(f"Classement des canaux par importance : {sorted_idx}")

            band_selection = [4, 3, 2]  # RGB classique
            feedback.pushInfo(f"Utilisation automatique des bandes : {band_selection} (RGB)")
        else:
            band_selection = [4, 3, 2] if band_set == 0 else [8, 4, 3]
            feedback.pushInfo(f"Utilisation manuelle des bandes : {band_selection}")

        # --- Lecture des bandes sélectionnées ---
        img_list = []
        for b in band_selection:
            band = ds.GetRasterBand(b)
            arr = band.ReadAsArray().astype(np.float32)
            img_list.append(arr)
        img = np.stack(img_list, axis=0)

        # Normalisation
        feedback.pushInfo("Normalisation des valeurs...")
        img = (img - img.min()) / (img.max() - img.min() + 1e-6)

        # --- Calcul NDWI si option activée ---
        if mask_land and ds.RasterCount >= 8:
            try:
                B3 = ds.GetRasterBand(3).ReadAsArray().astype(np.float32)
                B8 = ds.GetRasterBand(8).ReadAsArray().astype(np.float32)
                ndwi = (B3 - B8) / (B3 + B8 + 1e-6)
                water_mask = ndwi > 0
                feedback.pushInfo("NDWI calculé : les zones terrestres seront exclues.")
            except Exception as e:
                water_mask = np.ones((nrows, ncols), dtype=bool)
                feedback.pushInfo(f"NDWI non calculable ({e}), toutes les zones seront traitées.")
        else:
            water_mask = np.ones((nrows, ncols), dtype=bool)
            feedback.pushInfo("Masquage terrestre désactivé.")

        # --- Traitement par blocs ---
        block_size = 2048
        overlap = 512
        output_mask = np.zeros((nrows, ncols), dtype=np.float32)
        weight = np.zeros((nrows, ncols), dtype=np.float32)

        total_blocks = ((nrows // (block_size - overlap) + 1) *
                        (ncols // (block_size - overlap) + 1))
        done = 0
        feedback.pushInfo("Début du traitement par blocs...")

        for y in range(0, nrows, block_size - overlap):
            for x in range(0, ncols, block_size - overlap):
                if feedback.isCanceled():
                    break

                block = img[:, y:y+block_size, x:x+block_size]
                if block.size == 0:
                    continue

                mask_block_water = water_mask[y:y+block_size, x:x+block_size]
                if not mask_block_water.any():
                    continue

                # ======= BLOC ORIGINAL PRÉSERVÉ =======
                with torch.no_grad():
                    block_tensor = torch.from_numpy(block).unsqueeze(0)
                    pred = model(block_tensor)
                    mask_block = pred.squeeze().numpy()

                h, w = mask_block.shape
                mask_block *= mask_block_water[:h, :w]

                # Fenêtre Hanning
                win_y = windows.hann(h)[:, None]
                win_x = windows.hann(w)[None, :]
                weight_block = win_y * win_x

                output_mask[y:y+h, x:x+w] += mask_block * weight_block
                weight[y:y+h, x:x+w] += weight_block

                done += 1
                progress = int(100 * done / total_blocks)
                feedback.setProgress(progress)
                # ======= FIN DU BLOC ORIGINAL =======

        # Fusion finale
        output_mask /= np.maximum(weight, 1e-6)
        output_mask = np.clip(output_mask, 0, 1)

        # --- Sauvegarde GeoTIFF ---
        feedback.pushInfo("Sauvegarde du masque final...")
        driver = gdal.GetDriverByName("GTiff")
        out_path = self.parameterAsOutputLayer(parameters, "output_raster", context)
        out_ds = driver.Create(out_path, ncols, nrows, 1, gdal.GDT_Float32)
        out_ds.SetGeoTransform(ds.GetGeoTransform())
        out_ds.SetProjection(ds.GetProjection())
        out_ds.GetRasterBand(1).WriteArray(output_mask)
        out_ds.FlushCache()

        feedback.pushInfo("✅ Segmentation terminée avec succès.")
        return {"output_raster": out_path}

    # --- Métadonnées ---
    def name(self):
        return "segmentation_coraux"

    def displayName(self):
        return self.tr("Segmentation des images Sentinel 2 à 1m (U-Net)")

    def group(self):
        return self.tr("Deep Learning")

    def groupId(self):
        return "deeplearning"

    def tr(self, string):
        return QCoreApplication.translate("SegmentationCoraux", string)

    def createInstance(self):
        return SegmentationCoraux()

En un próximo artículo encontrará explicaciones sobre las diferentes partes del código.

El script QGIS que se presenta aquí permite aplicar modelos de segmentación de imágenes basados en U‑Net guardados en formato PyTorch (.pth). Está diseñado para procesar imágenes multiespectrales, como las procedentes de los satélites Sentinel‑2, y adapta automáticamente las bandas utilizadas en función del número de canales de entrada que espera el modelo. Por ejemplo, si el modelo se entrena con imágenes RGB, el script seleccionará las bandas Rojo, Verde y Azul; si el modelo espera más canales, propone una selección manual o utiliza todas las bandas disponibles. El código también incluye la posibilidad de ocultar las zonas terrestres mediante el cálculo del NDWI, lo que permite centrar la segmentación en las zonas acuáticas, por ejemplo, para identificar los corales. En la práctica, cualquier modelo U-Net PyTorch correctamente guardado puede cargarse y aplicarse, siempre que la arquitectura y los pesos estén incluidos en el archivo .pth.


Uso

Ejecute el script. Se abrirá la siguiente ventana:

La selección manual de bandas solo se utiliza si el script no puede determinar, a partir del modelo, qué bandas se deben utilizar.


Interpretación del resultado

La trama resultante del modelo representa un mapa de probabilidad:
los valores cercanos a 1 indican una fuerte presencia de corales, mientras que los cercanos a 0 corresponden a zonas sin corales (arena, algas, profundidad, etc.).

Una simbología adaptada (del azul claro al rojo) permite visualizar fácilmente la distribución espacial de las zonas con probable presencia de corales.
Al combinar este mapa con otros datos (batimetría, sustrato, turbidez), es posible estimar la vulnerabilidad o la degradación de los arrecifes a lo largo del tiempo.


Ventajas de este enfoque

La integración de PyTorch en QGIS abre nuevas perspectivas para la cartografía medioambiental asistida por inteligencia artificial:

  • Código abierto y reproducible: todo el proceso puede compartirse, modificarse o adaptarse a otras zonas costeras.
  • Autonomía local: no se necesita ArcGIS Pro ni una costosa licencia para probar o aplicar modelos de aprendizaje profundo.
  • Experimentación flexible: se pueden probar otras arquitecturas (SegNet, DeepLabV3, etc.) o adaptar el preprocesamiento a las especificidades de cada zona.


Hacia un ecosistema de modelos abiertos

A largo plazo, se podría imaginar una biblioteca compartida de modelos medioambientales de código abierto —el equivalente libre del formato DLPK— en la que cada .pth iría acompañado de su archivo de descripción (bandas, normalización, clases).

QGIS podría entonces ofrecer una interfaz para importar, probar y documentar estos modelos, facilitando su reutilización en contextos tropicales, costeros o forestales.


Conclusión

El auge del aprendizaje profundo marca una nueva etapa en la evolución de la geomática.

Mientras que el procesamiento clásico de imágenes se basaba en umbrales e índices espectrales, los modelos neuronales ahora aprenden a reconocer formas, texturas y firmas complejas directamente en los píxeles.

Gracias a herramientas como ArcGIS Pro (con sus DLPK) y QGIS (a través de PyTorch y scripts personalizados), esta potencia está al alcance de todos: investigadores, técnicos o aficionados a la cartografía medioambiental.

El ejemplo que se presenta aquí —la segmentación de corales a partir de imágenes Sentinel-2— ilustra el potencial de estos enfoques para el análisis detallado de los entornos costeros y la preservación de los ecosistemas marinos.

El reto ya no es solo técnico, sino también colectivo: poner en común los modelos, documentar sus entradas, compartir los métodos y hacer que el aprendizaje profundo sea más transparente, reproducible y abierto.

El futuro de la teledetección se construirá probablemente en la encrucijada de estos dos mundos —la ingeniería de software y el conocimiento del terreno— para transformar los datos satelitales en verdaderos indicadores del estado ecológico.


Si cet article vous a intéressé et que vous pensez qu'il pourrait bénéficier à d'autres personnes, n'hésitez pas à le partager sur vos réseaux sociaux en utilisant les boutons ci-dessous. Votre partage est apprécié !

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *