PyQGIS · Material de apoyo para la clase de Programación en Ambientes de SIG (PASIG)

Ejercicio 11: Complemento para calcular índices espectrales Landsat

En los ejercicios 4, 5 y 6 creaste herramientas de procesamiento para calcular los índices NDVI, SAVI, NDMI y NBR con imágenes Landsat 8 o 9. Este ejercicio reúne los cuatro cálculos en un solo complemento llamado "Índices Landsat". Su ventana permite elegir qué índices calcular; según la selección, habilita solo los campos de las bandas que se necesitan, y al aceptar calcula cada índice con la calculadora ráster y carga los resultados en QGIS.

Como en los ejercicios 9 y 10, Plugin Builder generó la estructura y el creador personalizó el resto. Este es el ejercicio con más lógica propia de la serie: las partes se distinguen con franjas de color en los bloques de código.

  • Franja amarilla · Personalizado por el creador. Datos que el creador escribió en el formulario de Plugin Builder: nombres de la clase y del módulo, descripción, autoría, texto del menú, etc.
  • Franja azul · Código agregado o modificado después de generar la plantilla. Todo lo que el creador programó o diseñó a mano: importaciones, métodos propios, conexiones de botones y elementos de la ventana.

Nota: las franjas se identificaron comparando los archivos con la plantilla estándar de Plugin Builder (tipo Tool button with dialog). Todo lo que no tiene franja es código de la plantilla que no fue alterado.

Conceptos clave de POO, PyQGIS y PyQt usados en el ejercicio

  • Una clase con muchos métodos propios: la clase IndicesLandsat agrupa la lógica en métodos pequeños con una sola responsabilidad: seleccionar bandas, habilitar campos, calcular y cancelar.
  • Casillas (QCheckBox) y estado de los elementos: isChecked() responde si una casilla está marcada; setChecked(False) la desmarca. Con setEnabled(True/False) se habilita o se deshabilita un elemento, que aparece atenuado y no se puede usar.
  • Interfaz que reacciona al usuario: las casillas están conectadas a métodos que habilitan o deshabilitan las etiquetas, cuadros de texto y botones de cada banda, para que el usuario solo vea disponibles los datos que realmente necesita.
  • Señales de varias fuentes hacia un mismo método: las casillas NDVI y SAVI están conectadas al mismo método (seleccionar_ndvi_savi) porque usan las mismas bandas.
  • Cadenas con formato (f-strings): f'("{b5}@1" - "{b4}@1") / ("{b5}@1" + "{b4}@1")' inserta el valor de las variables dentro del texto de la fórmula. La letra f antes de las comillas activa esa función.
  • Calculadora ráster desde código: processing.run('qgis:rastercalculator', ...) evalúa una expresión de álgebra de mapas sobre las bandas elegidas.
  • Mensajes al usuario: self.iface.messageBar().pushMessage(...) muestra un aviso en la barra de mensajes de QGIS.

Los archivos del complemento

La carpeta indices_lansat (el nombre de la carpeta conserva la escritura original) tiene la misma organización que las de los ejercicios 9 y 10. Los archivos que se analizan en esta página son:

  • __init__.py: punto de entrada; QGIS lo busca primero.
  • metadata.txt: ficha del complemento.
  • indices_lansat.py: la clase principal. Aquí está casi todo el código nuevo.
  • indices_lansat_dialog.py: la clase de la ventana.
  • indices_lansat_dialog_base.ui: el diseño de la ventana, con un grupo de casillas, campos para las bandas, prefijo y carpeta de salida.
  • pb_tool.cfg: configuración para desplegar el complemento (se revisa al final).

El resto de la carpeta (icon.png, Makefile, pylintrc, scripts/, help/, i18n/, test/ y README) lo generó Plugin Builder y no se modificó; se describe en el ejercicio 9.

Qué bandas necesita cada índice

La lógica de la ventana se basa en esta tabla (Landsat 8 y 9):

  • NDVI: banda 4 (rojo) y banda 5 (infrarrojo cercano, NIR).
  • SAVI: banda 4 y banda 5, las mismas que NDVI.
  • NDMI: banda 5 (NIR) y banda 6 (infrarrojo de onda corta 1, SWIR 1).
  • NBR: banda 5 (NIR) y banda 7 (infrarrojo de onda corta 2, SWIR 2).

Por eso la banda 5 es necesaria para los cuatro índices; la banda 4 solo para NDVI y SAVI; la 6 solo para NDMI; y la 7 solo para NBR.

Análisis y explicación de la estructura y lógica del código

Sección 1 · __init__.py: el punto de entrada

Igual que en los ejercicios anteriores: QGIS llama a classFactory, que crea y devuelve el objeto del complemento.

__init__.py
# -*- coding: utf-8 -*-
"""
/***************************************************************************
 IndicesLandsat
                                 A QGIS plugin
 Este complemento calcula los índices NDVI, SAVI, NDMI y NBR
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                             -------------------
        begin                : 2026-10-02
        copyright            : (C) 2026 by UAEM
        email                : indiceslandsat@uaemex.mx
        git sha              : $Format:%H$
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
 This script initializes the plugin, making it known to QGIS.
"""


# noinspection PyPep8Naming
def classFactory(iface):  # pylint: disable=invalid-name
    """Load IndicesLandsat class from file IndicesLandsat.

    :param iface: A QGIS interface instance.
    :type iface: QgsInterface
    """
    #
    from .indices_lansat import IndicesLandsat
    return IndicesLandsat(iface)
  • def classFactory(iface):: función con nombre fijo que QGIS ejecuta al cargar el complemento.
  • from .indices_lansat import IndicesLandsat: importación relativa de la clase principal desde indices_lansat.py. Observa que el módulo se escribe indices_lansat y la clase IndicesLandsat (con "d" y con "n"): son los valores Module name y Class name del formulario.
  • return IndicesLandsat(iface): crea el objeto y lo devuelve a QGIS.

Sección 2 · metadata.txt: la ficha del complemento

metadata.txt
# This file contains metadata for your plugin.

# This file should be included when you package your plugin.# Mandatory items:

[general]
name=Índices Landsat
qgisMinimumVersion=3.0
description=Este complemento calcula los índices NDVI, SAVI, NDMI y NBR
version=0.1
author=UAEM
email=indiceslandsat@uaemex.mx

about=Complemento para calcular los índices Landsat NDVI, SAVI, NDMI y NBR

tracker=https://www.uaemex.mx
repository=https://www.uaemex.mx
# End of mandatory metadata

# Recommended items:

hasProcessingProvider=no
# Uncomment the following line and add your changelog:
# changelog=

# Tags are comma separated with spaces allowed
tags=Landsat, NDVI, SAVI, NDMI, NBR

homepage=https://www.uaemex.mx
category=Plugins
icon=icon.png
# experimental flag
experimental=True

# deprecated flag (applies to the whole plugin, not just a single version)
deprecated=False

# Since QGIS 3.8, a comma separated list of plugins to be installed
# (or upgraded) can be specified.
# Check the documentation for more information.
# plugin_dependencies=

Category of the plugin: Raster, Vector, Database or Web
# category=

# If the plugin can run on QGIS Server.
server=False
  • name=Índices Landsat, description y about: nombre y descripciones que se ven en el Administrador de complementos. Personalizados.
  • author, email, tracker, repository, homepage: autoría y direcciones web. Personalizados.
  • tags=Landsat, NDVI, SAVI, NDMI, NBR: palabras clave para las búsquedas; ahora son varias y específicas. Personalizadas.
  • El resto (qgisMinimumVersion, version, hasProcessingProvider, icon, experimental, deprecated, server) proviene de la plantilla, como en el ejercicio 9.

Sección 3 · indices_lansat.py: la clase principal

Se muestra completo y después se explica por partes: importaciones, lo que solo cambia de nombre, los métodos que controlan la ventana, el método de cálculo y el método run.

indices_lansat.py
# -*- coding: utf-8 -*-
"""
/***************************************************************************
 IndicesLandsat
                                 A QGIS plugin
 Este complemento calcula los índices NDVI, SAVI, NDMI y NBR
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                              -------------------
        begin                : 2026-10-02
        git sha              : $Format:%H$
        copyright            : (C) 2026 by UAEM
        email                : indiceslandsat@uaemex.mx
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
"""
from qgis.PyQt.QtCore import QLocale, QTranslator, QCoreApplication
from qgis.core import QgsSettings
from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtWidgets import QAction

# Import the code for the dialog
from .indices_lansat_dialog import IndicesLandsatDialog
import os.path


#Importación de las Bibliotecas necesarias para nuestro complemento y para los métodos creados por nosotros
from qgis.PyQt.QtWidgets import QLineEdit, QCheckBox, QFileDialog
from qgis import processing
from qgis.core import QgsRasterLayer, QgsProject, Qgis

class IndicesLandsat:
    """QGIS Plugin Implementation."""

    def __init__(self, iface):
        """Constructor.

        :param iface: An interface instance that will be passed to this class
            which provides the hook by which you can manipulate the QGIS
            application at run time.
        :type iface: QgsInterface
        """
        # Save reference to the QGIS interface
        self.iface = iface
        # initialize plugin directory
        self.plugin_dir = os.path.dirname(__file__)
        # initialize locale
        locale = QgsSettings().value('locale/userLocale', QLocale().name())[0:2]
        locale_path = os.path.join(
            self.plugin_dir,
            'i18n',
            '{}.qm'.format(locale))

        if os.path.exists(locale_path):
            self.translator = QTranslator()
            self.translator.load(locale_path)
            QCoreApplication.installTranslator(self.translator)

        # Declare instance attributes
        self.actions = []
        self.menu = self.tr(u'&Índices Landsat')

        # Check if plugin was started the first time in current QGIS session
        # Must be set in initGui() to survive plugin reloads
        self.first_start = None

    # noinspection PyMethodMayBeStatic
    def tr(self, message):
        """Get the translation for a string using Qt translation API.

        We implement this ourselves since we do not inherit QObject.

        :param message: String for translation.
        :type message: str, QString

        :returns: Translated version of message.
        :rtype: QString
        """
        # noinspection PyTypeChecker,PyArgumentList,PyCallByClass
        return QCoreApplication.translate('IndicesLandsat', message)


    def add_action(
        self,
        icon_path,
        text,
        callback,
        enabled_flag=True,
        add_to_menu=True,
        add_to_toolbar=True,
        status_tip=None,
        whats_this=None,
        parent=None):
        """Add a toolbar icon to the toolbar.

        :param icon_path: Path to the icon for this action. Can be a resource
            path (e.g. ':/plugins/foo/bar.png') or a normal file system path.
        :type icon_path: str

        :param text: Text that should be shown in menu items for this action.
        :type text: str

        :param callback: Function to be called when the action is triggered.
        :type callback: function

        :param enabled_flag: A flag indicating if the action should be enabled
            by default. Defaults to True.
        :type enabled_flag: bool

        :param add_to_menu: Flag indicating whether the action should also
            be added to the menu. Defaults to True.
        :type add_to_menu: bool

        :param add_to_toolbar: Flag indicating whether the action should also
            be added to the toolbar. Defaults to True.
        :type add_to_toolbar: bool

        :param status_tip: Optional text to show in a popup when mouse pointer
            hovers over the action.
        :type status_tip: str

        :param parent: Parent widget for the new action. Defaults None.
        :type parent: QWidget

        :param whats_this: Optional text to show in the status bar when the
            mouse pointer hovers over the action.

        :returns: The action that was created. Note that the action is also
            added to self.actions list.
        :rtype: QAction
        """

        icon = QIcon(icon_path)
        action = QAction(icon, text, parent)
        action.triggered.connect(callback)
        action.setEnabled(enabled_flag)

        if status_tip is not None:
            action.setStatusTip(status_tip)

        if whats_this is not None:
            action.setWhatsThis(whats_this)

        if add_to_toolbar:
            # Adds plugin icon to Plugins toolbar
            self.iface.addToolBarIcon(action)

        if add_to_menu:
            self.iface.addPluginToMenu(
                self.menu,
                action)

        self.actions.append(action)

        return action

    def initGui(self):
        """Create the menu entries and toolbar icons inside the QGIS GUI."""

        icon_path = os.path.join(os.path.dirname(__file__), 'icon.png')
        self.add_action(
            icon_path,
            text=self.tr(u'Índices Landsat'),
            callback=self.run,
            parent=self.iface.mainWindow())

        # will be set False in run()
        self.first_start = True


    def unload(self):
        """Removes the plugin menu item and icon from QGIS GUI."""
        for action in self.actions:
            self.iface.removePluginMenu(
                self.tr(u'&Índices Landsat'),
                action)
            self.iface.removeToolBarIcon(action)

    def cancelar(self):
        #Buscar todos los widgewts de LineEdit dentro de la GUI y limpiar su contenido
        for widget in self.dlg.findChildren(QLineEdit):
            widget.clear()
        #Buscar todos los widgets de CheckBox de la GUI y desmarcarlos
        for widget in self.dlg.findChildren(QCheckBox):
            widget.setChecked(False)
        #Cerrar la GUI
        self.dlg.close()

    def seleccionar_ndvi_savi(self):
        #Se trabaja junto NDVI y SAVI porque usan las mismas bandas para su cálculo
        if self.dlg.checkBox_ndvi.isChecked() or self.dlg.checkBox_savi.isChecked():
            self.dlg.label_b4.setEnabled(True)
            self.dlg.lineEdit_b4.setEnabled(True)
            self.dlg.pushButton_b4.setEnabled(True)
            self.dlg.label_b5.setEnabled(True)
            self.dlg.lineEdit_b5.setEnabled(True)
            self.dlg.pushButton_b5.setEnabled(True)
        else:
            if self.dlg.checkBox_ndmi.isChecked():
                self.dlg.label_b4.setEnabled(False)
                self.dlg.lineEdit_b4.setEnabled(False)
                self.dlg.pushButton_b4.setEnabled(False)
                self.dlg.label_b5.setEnabled(True)
                self.dlg.lineEdit_b5.setEnabled(True)
                self.dlg.pushButton_b5.setEnabled(True)
            else:
                if self.dlg.checkBox_nbr.isChecked():
                    self.dlg.label_b4.setEnabled(False)
                    self.dlg.lineEdit_b4.setEnabled(False)
                    self.dlg.pushButton_b4.setEnabled(False)
                    self.dlg.label_b5.setEnabled(True)
                    self.dlg.lineEdit_b5.setEnabled(True)
                    self.dlg.pushButton_b5.setEnabled(True)
                else:
                    self.dlg.label_b4.setEnabled(False)
                    self.dlg.lineEdit_b4.setEnabled(False)
                    self.dlg.pushButton_b4.setEnabled(False)
                    self.dlg.label_b5.setEnabled(False)
                    self.dlg.lineEdit_b5.setEnabled(False)
                    self.dlg.pushButton_b5.setEnabled(False)
    
    def seleccionar_ndmi(self):
        if self.dlg.checkBox_ndmi.isChecked():
            self.dlg.label_b5.setEnabled(True)
            self.dlg.lineEdit_b5.setEnabled(True)
            self.dlg.pushButton_b5.setEnabled(True)
            self.dlg.label_b6.setEnabled(True)
            self.dlg.lineEdit_b6.setEnabled(True)
            self.dlg.pushButton_b6.setEnabled(True)
        else:
            if self.dlg.checkBox_ndvi.isChecked() or self.dlg.checkBox_savi.isChecked() or self.dlg.checkBox_nbr.isChecked():
                self.dlg.label_b5.setEnabled(True)
                self.dlg.lineEdit_b5.setEnabled(True)
                self.dlg.pushButton_b5.setEnabled(True)
                self.dlg.label_b6.setEnabled(False)
                self.dlg.lineEdit_b6.setEnabled(False)
                self.dlg.pushButton_b6.setEnabled(False)
            else:
                self.dlg.label_b5.setEnabled(False)
                self.dlg.lineEdit_b5.setEnabled(False)
                self.dlg.pushButton_b5.setEnabled(False)
                self.dlg.label_b6.setEnabled(False)
                self.dlg.lineEdit_b6.setEnabled(False)
                self.dlg.pushButton_b6.setEnabled(False)

    def seleccionar_nbr(self):
            if self.dlg.checkBox_nbr.isChecked():
                self.dlg.label_b5.setEnabled(True)
                self.dlg.lineEdit_b5.setEnabled(True)
                self.dlg.pushButton_b5.setEnabled(True)
                self.dlg.label_b7.setEnabled(True)
                self.dlg.lineEdit_b7.setEnabled(True)
                self.dlg.pushButton_b7.setEnabled(True)
            else:
                if self.dlg.checkBox_ndvi.isChecked() or self.dlg.checkBox_savi.isChecked() or self.dlg.checkBox_ndmi.isChecked():
                    self.dlg.label_b5.setEnabled(True)
                    self.dlg.lineEdit_b5.setEnabled(True)
                    self.dlg.pushButton_b5.setEnabled(True)
                    self.dlg.label_b7.setEnabled(False)
                    self.dlg.lineEdit_b7.setEnabled(False)
                    self.dlg.pushButton_b7.setEnabled(False)
                else:
                    self.dlg.label_b5.setEnabled(False)
                    self.dlg.lineEdit_b5.setEnabled(False)
                    self.dlg.pushButton_b5.setEnabled(False)
                    self.dlg.label_b7.setEnabled(False)
                    self.dlg.lineEdit_b7.setEnabled(False)
                    self.dlg.pushButton_b7.setEnabled(False)

    def capa_b4(self):
        b4 = QFileDialog.getOpenFileName(self.dlg, 'Seleccionar la capa de la banda 4 (Red)', '', 'Archivos TIFF (*.tif)')[0]
        self.dlg.lineEdit_b4.setText(b4)

    def capa_b5(self):
        b5 = QFileDialog.getOpenFileName(self.dlg, 'Seleccionar la capa de la banda 5 (NIR)', '', 'Archivos TIFF (*.tif)')[0]
        self.dlg.lineEdit_b5.setText(b5)

    def capa_b6(self):
        b6 = QFileDialog.getOpenFileName(self.dlg, 'Seleccionar la capa de la banda 6 (SWIR 1)', '', 'Archivos TIFF (*.tif)')[0]
        self.dlg.lineEdit_b6.setText(b6)

    def capa_b7(self):
        b7 = QFileDialog.getOpenFileName(self.dlg, 'Seleccionar la capa de la banda 7 (SWIR 2)', '', 'Archivos TIFF (*.tif)')[0]
        self.dlg.lineEdit_b7.setText(b7)

    def carpeta_salidas(self):
        carpeta = QFileDialog.getExistingDirectory(self.dlg, 'Seleccionar carpeta de salidas')
        self.dlg.lineEdit_salidas.setText(carpeta + '/')

    def aceptar(self):
        #Este es el método que hace el geoprocesamiento principal
        #Procesar NDVI
        if self.dlg.checkBox_ndvi.isChecked():
            b4 = self.dlg.lineEdit_b4.text()
            b5 = self.dlg.lineEdit_b5.text()
            ndvi = self.dlg.lineEdit_salidas.text() + self.dlg.lineEdit_prefijo.text() + '_ndvi.tif'
            #Calculadora ráster
            alg_params = {'EXPRESSION':f'("{b5}@1" - "{b4}@1") / ("{b5}@1" + "{b4}@1")',
                        'LAYERS':[b4, b5],
                        'CELLSIZE': 0,
                        'EXTENT':None,
                        'CRS':None,
                        'OUTPUT':ndvi}
            processing.run('qgis:rastercalculator', alg_params)
            #Cargar el resultado en el proyecto de QGIS
            capa_ndvi = QgsRasterLayer(ndvi, self.dlg.lineEdit_prefijo.text() + '_NDVI')
            QgsProject.instance().addMapLayer(capa_ndvi)

        #Procesar SAVI
        if self.dlg.checkBox_savi.isChecked():
            b4 = self.dlg.lineEdit_b4.text()
            b5 = self.dlg.lineEdit_b5.text()
            savi = self.dlg.lineEdit_salidas.text() + self.dlg.lineEdit_prefijo.text() + '_savi.tif'
            #Calculadora ráster
            alg_params = {'EXPRESSION':f'((("{b5}@1" - "{b4}@1") / ("{b5}@1" + "{b4}@1" + 0.5)) * (1.5))',
                        'LAYERS':[b4, b5],
                        'CELLSIZE': 0,
                        'EXTENT':None,
                        'CRS':None,
                        'OUTPUT':savi}
            processing.run('qgis:rastercalculator', alg_params)
            #Cargar el resultado en el proyecto de QGIS
            capa_savi = QgsRasterLayer(savi, self.dlg.lineEdit_prefijo.text() + '_SAVI')
            QgsProject.instance().addMapLayer(capa_savi)

        #Procesar NDMI
        if self.dlg.checkBox_ndmi.isChecked():
            b5 = self.dlg.lineEdit_b5.text()
            b6 = self.dlg.lineEdit_b6.text()
            ndmi = self.dlg.lineEdit_salidas.text() + self.dlg.lineEdit_prefijo.text() + '_ndmi.tif'
            #Calculadora ráster
            alg_params = {'EXPRESSION': f'("{b5}@1" - "{b6}@1") / ("{b5}@1" + "{b6}@1")',
                        'LAYERS':[b5, b6],
                        'CELLSIZE': 0,
                        'EXTENT':None,
                        'CRS':None,
                        'OUTPUT':ndmi}
            processing.run('qgis:rastercalculator', alg_params)
            #Cargar el resultado en el proyecto de QGIS
            capa_ndmi = QgsRasterLayer(ndmi, self.dlg.lineEdit_prefijo.text() + '_NDMI')
            QgsProject.instance().addMapLayer(capa_ndmi)

        #Procesar NBR
        if self.dlg.checkBox_nbr.isChecked():
            b5 = self.dlg.lineEdit_b5.text()
            b7 = self.dlg.lineEdit_b7.text()
            nbr = self.dlg.lineEdit_salidas.text() + self.dlg.lineEdit_prefijo.text() + '_nbr.tif'
            #Calculadora ráster
            alg_params = {'EXPRESSION': f'("{b5}@1" - "{b7}@1") / ("{b5}@1" + "{b7}@1")',
                        'LAYERS':[b5, b7],
                        'CELLSIZE': 0,
                        'EXTENT':None,
                        'CRS':None,
                        'OUTPUT':nbr}
            processing.run('qgis:rastercalculator', alg_params)
            #Cargar el resultado en el proyecto de QGIS
            capa_nbr = QgsRasterLayer(nbr, self.dlg.lineEdit_prefijo.text() + '_NBR')
            QgsProject.instance().addMapLayer(capa_nbr)

        #Mensaje de finalización del procesamiento
        self.iface.messageBar().pushMessage("Finalizado", "Cálculo de índices espectrales finalizado.", level=Qgis.Success, duration=7)

    def run(self):
        """Run method that performs all the real work"""

        # Create the dialog with elements (after translation) and keep reference
        # Only create GUI ONCE in callback, so that it will only load when the plugin is started
        if self.first_start == True:
            self.first_start = False
            self.dlg = IndicesLandsatDialog()

            #Acciones en nuestra GUI para llamar a los métodos creados por nosotros
            #Botón Cancelar
            self.dlg.pushButton_cancelar.clicked.connect(self.cancelar)
            #CheckBox de NDVIy SAVI
            self.dlg.checkBox_ndvi.clicked.connect(self.seleccionar_ndvi_savi)
            self.dlg.checkBox_savi.clicked.connect(self.seleccionar_ndvi_savi)
            #CheckBox de NDMI
            self.dlg.checkBox_ndmi.clicked.connect(self.seleccionar_ndmi)
            #CheckBox de NBR
            self.dlg.checkBox_nbr.clicked.connect(self.seleccionar_nbr)
            #Cargar archivo de la banda 4
            self.dlg.pushButton_b4.clicked.connect(self.capa_b4)
            #Cargar archivo de la banda 4
            self.dlg.pushButton_b5.clicked.connect(self.capa_b5)
            #Cargar archivo de la banda 4
            self.dlg.pushButton_b6.clicked.connect(self.capa_b6)
            #Cargar archivo de la banda 4
            self.dlg.pushButton_b7.clicked.connect(self.capa_b7)
            #Seleccionar carpeta para guardar los resultados
            self.dlg.pushButton_salidas.clicked.connect(self.carpeta_salidas)
            #Aceptar para iniciar el geoproceso
            self.dlg.pushButton_aceptar.clicked.connect(self.aceptar)

        # show the dialog
        self.dlg.show()

        

3.1 · Para qué sirve cada biblioteca importada

Las importaciones de la plantilla (QtCore, QgsSettings, QIcon, QAction, os.path) se explican en el ejercicio 9. El creador agregó tres líneas:

indices_lansat.py (fragmento)
#Importación de las Bibliotecas necesarias para nuestro complemento y para los métodos creados por nosotros
from qgis.PyQt.QtWidgets import QLineEdit, QCheckBox, QFileDialog
from qgis import processing
from qgis.core import QgsRasterLayer, QgsProject, Qgis
  • from qgis.PyQt.QtWidgets import QLineEdit, QCheckBox, QFileDialog: importa solo las tres clases visuales que se usan: QLineEdit (cuadros de texto, para buscarlos y limpiarlos), QCheckBox (casillas, para desmarcarlas) y QFileDialog (ventanas para elegir archivos y carpetas). A diferencia del ejercicio 10, no se usa el asterisco.
  • from qgis import processing: módulo para ejecutar herramientas de QGIS con processing.run; aquí, la calculadora ráster.
  • from qgis.core import QgsRasterLayer, QgsProject, Qgis:
    • QgsRasterLayer: crea una capa ráster a partir de un archivo.
    • QgsProject: el proyecto abierto en QGIS, para agregarle las capas resultantes.
    • Qgis: contiene constantes generales de QGIS; aquí se usa Qgis.Success para el tipo del mensaje final.

3.2 · Lo que de la plantilla solo cambia de nombre

__init__, tr, add_action, initGui y unload son los de la plantilla (ejercicio 9). Solo cambian los textos del formulario (franjas amarillas):

  • class IndicesLandsat:: nombre de la clase.
  • self.menu = self.tr(u'&Índices Landsat'): nombre del menú del complemento.
  • QCoreApplication.translate('IndicesLandsat', message): contexto de traducción (nombre de la clase).
  • text=self.tr(u'Índices Landsat') en initGui: texto de la opción del menú.
  • self.tr(u'&Índices Landsat') en unload: debe coincidir con self.menu para que la opción se elimine correctamente.

3.3 · Para qué sirve el método cancelar

indices_lansat.py (fragmento)
    def cancelar(self):
        #Buscar todos los widgewts de LineEdit dentro de la GUI y limpiar su contenido
        for widget in self.dlg.findChildren(QLineEdit):
            widget.clear()
        #Buscar todos los widgets de CheckBox de la GUI y desmarcarlos
        for widget in self.dlg.findChildren(QCheckBox):
            widget.setChecked(False)
        #Cerrar la GUI
        self.dlg.close()
  • for widget in self.dlg.findChildren(QLineEdit): widget.clear(): busca todos los cuadros de texto de la ventana y los vacía.
  • for widget in self.dlg.findChildren(QCheckBox): widget.setChecked(False): busca todas las casillas y las desmarca.
  • self.dlg.close(): cierra la ventana.
  • Observación: setChecked(False) cambia la casilla desde el código y no emite la señal clicked (esa señal solo se emite cuando el usuario hace clic). Por eso, al volver a abrir la ventana tras cancelar, las casillas están desmarcadas pero los campos de las bandas que estaban habilitados siguen habilitados.

3.4 · Los métodos que habilitan o deshabilitan los campos de las bandas

Cada banda tiene tres elementos en la ventana: una etiqueta (label_b4), un cuadro de texto (lineEdit_b4) y un botón (pushButton_b4). En el diseño los tres inician deshabilitados. Tres métodos deciden cuáles se habilitan según las casillas marcadas.

seleccionar_ndvi_savi

indices_lansat.py (fragmento)
    def seleccionar_ndvi_savi(self):
        #Se trabaja junto NDVI y SAVI porque usan las mismas bandas para su cálculo
        if self.dlg.checkBox_ndvi.isChecked() or self.dlg.checkBox_savi.isChecked():
            self.dlg.label_b4.setEnabled(True)
            self.dlg.lineEdit_b4.setEnabled(True)
            self.dlg.pushButton_b4.setEnabled(True)
            self.dlg.label_b5.setEnabled(True)
            self.dlg.lineEdit_b5.setEnabled(True)
            self.dlg.pushButton_b5.setEnabled(True)
        else:
            if self.dlg.checkBox_ndmi.isChecked():
                self.dlg.label_b4.setEnabled(False)
                self.dlg.lineEdit_b4.setEnabled(False)
                self.dlg.pushButton_b4.setEnabled(False)
                self.dlg.label_b5.setEnabled(True)
                self.dlg.lineEdit_b5.setEnabled(True)
                self.dlg.pushButton_b5.setEnabled(True)
            else:
                if self.dlg.checkBox_nbr.isChecked():
                    self.dlg.label_b4.setEnabled(False)
                    self.dlg.lineEdit_b4.setEnabled(False)
                    self.dlg.pushButton_b4.setEnabled(False)
                    self.dlg.label_b5.setEnabled(True)
                    self.dlg.lineEdit_b5.setEnabled(True)
                    self.dlg.pushButton_b5.setEnabled(True)
                else:
                    self.dlg.label_b4.setEnabled(False)
                    self.dlg.lineEdit_b4.setEnabled(False)
                    self.dlg.pushButton_b4.setEnabled(False)
                    self.dlg.label_b5.setEnabled(False)
                    self.dlg.lineEdit_b5.setEnabled(False)
                    self.dlg.pushButton_b5.setEnabled(False)
  • if self.dlg.checkBox_ndvi.isChecked() or self.dlg.checkBox_savi.isChecked():: si NDVI o SAVI está marcado, habilita los tres elementos de la banda 4 y de la banda 5 con setEnabled(True). Se tratan juntos porque comparten las bandas 4 y 5.
  • Si ninguno está marcado, el método revisa los otros índices para no deshabilitar lo que todavía se necesita:
    • Si NDMI está marcado: la banda 4 se deshabilita (no la usa) y la banda 5 se mantiene habilitada.
    • Si NBR está marcado: igual, banda 4 deshabilitada y banda 5 habilitada.
    • Si no hay ninguna casilla marcada: se deshabilitan ambas bandas.
  • Este método se conecta a dos casillas (NDVI y SAVI), porque cualquiera de ellas puede cambiar la situación de las bandas 4 y 5.

seleccionar_ndmi

indices_lansat.py (fragmento)
    def seleccionar_ndmi(self):
        if self.dlg.checkBox_ndmi.isChecked():
            self.dlg.label_b5.setEnabled(True)
            self.dlg.lineEdit_b5.setEnabled(True)
            self.dlg.pushButton_b5.setEnabled(True)
            self.dlg.label_b6.setEnabled(True)
            self.dlg.lineEdit_b6.setEnabled(True)
            self.dlg.pushButton_b6.setEnabled(True)
        else:
            if self.dlg.checkBox_ndvi.isChecked() or self.dlg.checkBox_savi.isChecked() or self.dlg.checkBox_nbr.isChecked():
                self.dlg.label_b5.setEnabled(True)
                self.dlg.lineEdit_b5.setEnabled(True)
                self.dlg.pushButton_b5.setEnabled(True)
                self.dlg.label_b6.setEnabled(False)
                self.dlg.lineEdit_b6.setEnabled(False)
                self.dlg.pushButton_b6.setEnabled(False)
            else:
                self.dlg.label_b5.setEnabled(False)
                self.dlg.lineEdit_b5.setEnabled(False)
                self.dlg.pushButton_b5.setEnabled(False)
                self.dlg.label_b6.setEnabled(False)
                self.dlg.lineEdit_b6.setEnabled(False)
                self.dlg.pushButton_b6.setEnabled(False)
  • Si NDMI está marcado, habilita las bandas 5 y 6.
  • Si se desmarca, la banda 6 se deshabilita; la banda 5 solo permanece habilitada si NDVI, SAVI o NBR siguen marcados (or encadenados). Si no hay ninguno marcado, se deshabilitan ambas.

seleccionar_nbr

indices_lansat.py (fragmento)
    def seleccionar_nbr(self):
            if self.dlg.checkBox_nbr.isChecked():
                self.dlg.label_b5.setEnabled(True)
                self.dlg.lineEdit_b5.setEnabled(True)
                self.dlg.pushButton_b5.setEnabled(True)
                self.dlg.label_b7.setEnabled(True)
                self.dlg.lineEdit_b7.setEnabled(True)
                self.dlg.pushButton_b7.setEnabled(True)
            else:
                if self.dlg.checkBox_ndvi.isChecked() or self.dlg.checkBox_savi.isChecked() or self.dlg.checkBox_ndmi.isChecked():
                    self.dlg.label_b5.setEnabled(True)
                    self.dlg.lineEdit_b5.setEnabled(True)
                    self.dlg.pushButton_b5.setEnabled(True)
                    self.dlg.label_b7.setEnabled(False)
                    self.dlg.lineEdit_b7.setEnabled(False)
                    self.dlg.pushButton_b7.setEnabled(False)
                else:
                    self.dlg.label_b5.setEnabled(False)
                    self.dlg.lineEdit_b5.setEnabled(False)
                    self.dlg.pushButton_b5.setEnabled(False)
                    self.dlg.label_b7.setEnabled(False)
                    self.dlg.lineEdit_b7.setEnabled(False)
                    self.dlg.pushButton_b7.setEnabled(False)
  • Es la misma lógica con la banda 7: NBR marcado habilita las bandas 5 y 7; al desmarcarlo se deshabilita la 7, y la 5 se conserva solo si NDVI, SAVI o NDMI siguen marcados.
  • Observación: los tres métodos repiten bloques casi idénticos de setEnabled. Funciona, pero un programador podría simplificarlo calculando primero qué bandas hacen falta (por ejemplo, con un conjunto de nombres) y habilitando cada elemento una sola vez. El código actual es más fácil de seguir para quien empieza.

3.5 · Los métodos para elegir archivos y carpeta

indices_lansat.py (fragmento)
    def capa_b4(self):
        b4 = QFileDialog.getOpenFileName(self.dlg, 'Seleccionar la capa de la banda 4 (Red)', '', 'Archivos TIFF (*.tif)')[0]
        self.dlg.lineEdit_b4.setText(b4)

    def capa_b5(self):
        b5 = QFileDialog.getOpenFileName(self.dlg, 'Seleccionar la capa de la banda 5 (NIR)', '', 'Archivos TIFF (*.tif)')[0]
        self.dlg.lineEdit_b5.setText(b5)

    def capa_b6(self):
        b6 = QFileDialog.getOpenFileName(self.dlg, 'Seleccionar la capa de la banda 6 (SWIR 1)', '', 'Archivos TIFF (*.tif)')[0]
        self.dlg.lineEdit_b6.setText(b6)

    def capa_b7(self):
        b7 = QFileDialog.getOpenFileName(self.dlg, 'Seleccionar la capa de la banda 7 (SWIR 2)', '', 'Archivos TIFF (*.tif)')[0]
        self.dlg.lineEdit_b7.setText(b7)

    def carpeta_salidas(self):
        carpeta = QFileDialog.getExistingDirectory(self.dlg, 'Seleccionar carpeta de salidas')
        self.dlg.lineEdit_salidas.setText(carpeta + '/')
  • def capa_b4(self): (y capa_b5, capa_b6, capa_b7): cada uno abre con QFileDialog.getOpenFileName(...) una ventana para elegir el archivo de la banda; el título indica cuál es y el filtro 'Archivos TIFF (*.tif)' limita la elección a archivos .tif.
  • [0]: la función devuelve una tupla (ruta, filtro); el índice [0] toma solo la ruta.
  • self.dlg.lineEdit_b4.setText(b4): escribe la ruta en el cuadro de texto de esa banda.
  • carpeta_salidas: usa QFileDialog.getExistingDirectory, que permite elegir una carpeta, no un archivo. Al texto se le agrega '/' para que después se pueda concatenar el nombre del archivo directamente.

3.6 · Para qué sirve el método aceptar: el cálculo de los índices

Contiene cuatro bloques independientes, uno por índice. Cada bloque se ejecuta solo si su casilla está marcada (if self.dlg.checkBox_...isChecked():), por lo que el usuario puede calcular uno, varios o todos los índices en una sola ejecución. Todos siguen los mismos cinco pasos; se explican con el primero (NDVI).

indices_lansat.py (fragmento: NDVI)
    def aceptar(self):
        #Este es el método que hace el geoprocesamiento principal
        #Procesar NDVI
        if self.dlg.checkBox_ndvi.isChecked():
            b4 = self.dlg.lineEdit_b4.text()
            b5 = self.dlg.lineEdit_b5.text()
            ndvi = self.dlg.lineEdit_salidas.text() + self.dlg.lineEdit_prefijo.text() + '_ndvi.tif'
            #Calculadora ráster
            alg_params = {'EXPRESSION':f'("{b5}@1" - "{b4}@1") / ("{b5}@1" + "{b4}@1")',
                        'LAYERS':[b4, b5],
                        'CELLSIZE': 0,
                        'EXTENT':None,
                        'CRS':None,
                        'OUTPUT':ndvi}
            processing.run('qgis:rastercalculator', alg_params)
            #Cargar el resultado en el proyecto de QGIS
            capa_ndvi = QgsRasterLayer(ndvi, self.dlg.lineEdit_prefijo.text() + '_NDVI')
            QgsProject.instance().addMapLayer(capa_ndvi)
  • b4 = self.dlg.lineEdit_b4.text() y b5 = ...: leen de la ventana las rutas de las bandas necesarias.
  • ndvi = self.dlg.lineEdit_salidas.text() + self.dlg.lineEdit_prefijo.text() + '_ndvi.tif': arma la ruta del archivo de salida uniendo (con +) la carpeta elegida, el prefijo que escribió el usuario y el sufijo _ndvi.tif. Así los resultados de distintas escenas no se sobrescriben.
  • 'EXPRESSION': f'("{b5}@1" - "{b4}@1") / ("{b5}@1" + "{b4}@1")': la fórmula del NDVI, (NIR − Rojo) / (NIR + Rojo). Cada capa se escribe entre comillas dobles seguida de @1, que significa "banda 1 de esa capa"; como cada archivo .tif tiene una sola banda, es la banda única. Las llaves { } de la f-string se sustituyen por las rutas.
  • 'LAYERS': [b4, b5]: lista de las capas que participan en la fórmula.
  • 'CELLSIZE': 0, 'EXTENT': None, 'CRS': None: valores que indican que la herramienta debe tomar el tamaño de celda, la extensión y el sistema de coordenadas de las capas de entrada.
  • 'OUTPUT': ndvi: ruta del archivo resultante.
  • processing.run('qgis:rastercalculator', alg_params): ejecuta la calculadora ráster.
  • QgsRasterLayer(ndvi, self.dlg.lineEdit_prefijo.text() + '_NDVI') y QgsProject.instance().addMapLayer(capa_ndvi): crean la capa a partir del archivo recién calculado, con el prefijo en su nombre, y la agregan al proyecto.

Las fórmulas de los otros índices

indices_lansat.py (fragmento: SAVI)
        #Procesar SAVI
        if self.dlg.checkBox_savi.isChecked():
            b4 = self.dlg.lineEdit_b4.text()
            b5 = self.dlg.lineEdit_b5.text()
            savi = self.dlg.lineEdit_salidas.text() + self.dlg.lineEdit_prefijo.text() + '_savi.tif'
            #Calculadora ráster
            alg_params = {'EXPRESSION':f'((("{b5}@1" - "{b4}@1") / ("{b5}@1" + "{b4}@1" + 0.5)) * (1.5))',
                        'LAYERS':[b4, b5],
                        'CELLSIZE': 0,
                        'EXTENT':None,
                        'CRS':None,
                        'OUTPUT':savi}
            processing.run('qgis:rastercalculator', alg_params)
            #Cargar el resultado en el proyecto de QGIS
            capa_savi = QgsRasterLayer(savi, self.dlg.lineEdit_prefijo.text() + '_SAVI')
            QgsProject.instance().addMapLayer(capa_savi)
  • SAVI: ((NIR − Rojo) / (NIR + Rojo + 0.5)) * 1.5. Usa las bandas 4 y 5 como el NDVI, pero incorpora un factor de ajuste del suelo (0.5) y un multiplicador (1.5), como en el ejercicio 5.
indices_lansat.py (fragmento: NDMI y NBR)
        #Procesar NDMI
        if self.dlg.checkBox_ndmi.isChecked():
            b5 = self.dlg.lineEdit_b5.text()
            b6 = self.dlg.lineEdit_b6.text()
            ndmi = self.dlg.lineEdit_salidas.text() + self.dlg.lineEdit_prefijo.text() + '_ndmi.tif'
            #Calculadora ráster
            alg_params = {'EXPRESSION': f'("{b5}@1" - "{b6}@1") / ("{b5}@1" + "{b6}@1")',
                        'LAYERS':[b5, b6],
                        'CELLSIZE': 0,
                        'EXTENT':None,
                        'CRS':None,
                        'OUTPUT':ndmi}
            processing.run('qgis:rastercalculator', alg_params)
            #Cargar el resultado en el proyecto de QGIS
            capa_ndmi = QgsRasterLayer(ndmi, self.dlg.lineEdit_prefijo.text() + '_NDMI')
            QgsProject.instance().addMapLayer(capa_ndmi)

        #Procesar NBR
        if self.dlg.checkBox_nbr.isChecked():
            b5 = self.dlg.lineEdit_b5.text()
            b7 = self.dlg.lineEdit_b7.text()
            nbr = self.dlg.lineEdit_salidas.text() + self.dlg.lineEdit_prefijo.text() + '_nbr.tif'
            #Calculadora ráster
            alg_params = {'EXPRESSION': f'("{b5}@1" - "{b7}@1") / ("{b5}@1" + "{b7}@1")',
                        'LAYERS':[b5, b7],
                        'CELLSIZE': 0,
                        'EXTENT':None,
                        'CRS':None,
                        'OUTPUT':nbr}
            processing.run('qgis:rastercalculator', alg_params)
            #Cargar el resultado en el proyecto de QGIS
            capa_nbr = QgsRasterLayer(nbr, self.dlg.lineEdit_prefijo.text() + '_NBR')
            QgsProject.instance().addMapLayer(capa_nbr)
  • NDMI: (NIR − SWIR1) / (NIR + SWIR1), con las bandas 5 y 6. Estima la humedad de la vegetación.
  • NBR: (NIR − SWIR2) / (NIR + SWIR2), con las bandas 5 y 7. Se usa para identificar áreas quemadas.

Mensaje final

indices_lansat.py (fragmento)
        #Mensaje de finalización del procesamiento
        self.iface.messageBar().pushMessage("Finalizado", "Cálculo de índices espectrales finalizado.", level=Qgis.Success, duration=7)
  • self.iface.messageBar(): obtiene la barra de mensajes de QGIS, ubicada sobre el mapa.
  • .pushMessage("Finalizado", "Cálculo de índices espectrales finalizado.", level=Qgis.Success, duration=7): muestra un aviso con título y texto, con el estilo de éxito (Qgis.Success, verde), que desaparece a los 7 segundos.

3.7 · Para qué sirve el método run

indices_lansat.py (fragmento)
    def run(self):
        """Run method that performs all the real work"""

        # Create the dialog with elements (after translation) and keep reference
        # Only create GUI ONCE in callback, so that it will only load when the plugin is started
        if self.first_start == True:
            self.first_start = False
            self.dlg = IndicesLandsatDialog()

            #Acciones en nuestra GUI para llamar a los métodos creados por nosotros
            #Botón Cancelar
            self.dlg.pushButton_cancelar.clicked.connect(self.cancelar)
            #CheckBox de NDVIy SAVI
            self.dlg.checkBox_ndvi.clicked.connect(self.seleccionar_ndvi_savi)
            self.dlg.checkBox_savi.clicked.connect(self.seleccionar_ndvi_savi)
            #CheckBox de NDMI
            self.dlg.checkBox_ndmi.clicked.connect(self.seleccionar_ndmi)
            #CheckBox de NBR
            self.dlg.checkBox_nbr.clicked.connect(self.seleccionar_nbr)
            #Cargar archivo de la banda 4
            self.dlg.pushButton_b4.clicked.connect(self.capa_b4)
            #Cargar archivo de la banda 4
            self.dlg.pushButton_b5.clicked.connect(self.capa_b5)
            #Cargar archivo de la banda 4
            self.dlg.pushButton_b6.clicked.connect(self.capa_b6)
            #Cargar archivo de la banda 4
            self.dlg.pushButton_b7.clicked.connect(self.capa_b7)
            #Seleccionar carpeta para guardar los resultados
            self.dlg.pushButton_salidas.clicked.connect(self.carpeta_salidas)
            #Aceptar para iniciar el geoproceso
            self.dlg.pushButton_aceptar.clicked.connect(self.aceptar)

        # show the dialog
        self.dlg.show()
  • if self.first_start == True:: la ventana se crea una sola vez (self.dlg = IndicesLandsatDialog()) y en ese mismo momento se conectan las señales.
  • self.dlg.pushButton_cancelar.clicked.connect(self.cancelar): botón Cancelar → método cancelar.
  • checkBox_ndvi.clicked.connect(self.seleccionar_ndvi_savi) y checkBox_savi.clicked...: las dos casillas llaman al mismo método.
  • checkBox_ndmi.clicked.connect(self.seleccionar_ndmi) y checkBox_nbr.clicked.connect(self.seleccionar_nbr): cada una con su método.
  • pushButton_b4, pushButton_b5, pushButton_b6 y pushButton_b7 → capa_b4, capa_b5, capa_b6 y capa_b7: abren la selección de cada banda. (Los comentarios del código de las bandas 5, 6 y 7 repiten "banda 4" por haberse copiado; la conexión de cada botón es la correcta.)
  • pushButton_salidas → carpeta_salidas y pushButton_aceptar → aceptar: elegir la carpeta de resultados y ejecutar el cálculo.
  • Todas las conexiones están dentro del if, para que se hagan una sola vez; de lo contrario cada clic ejecutaría el método varias veces.
  • self.dlg.show(): muestra la ventana. Como en el ejercicio 10, ya no se usa exec_() porque los botones propios sustituyen a los botones OK/Cancel de la plantilla.

Sección 4 · indices_lansat_dialog.py: la clase de la ventana

Igual que en los ejercicios 9 y 10, solo cambian los nombres escritos en el formulario.

indices_lansat_dialog.py
# -*- coding: utf-8 -*-
"""
/***************************************************************************
 IndicesLandsatDialog
                                 A QGIS plugin
 Este complemento calcula los índices NDVI, SAVI, NDMI y NBR
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                             -------------------
        begin                : 2026-10-02
        git sha              : $Format:%H$
        copyright            : (C) 2026 by UAEM
        email                : indiceslandsat@uaemex.mx
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
"""

import os

from qgis.PyQt import uic
from qgis.PyQt import QtWidgets

# This loads your .ui file so that PyQt can populate your plugin with the elements from Qt Designer
FORM_CLASS, _ = uic.loadUiType(
    os.path.join(
        os.path.dirname(__file__),
        'indices_lansat_dialog_base.ui'),
    from_imports=True,
    import_from='indices_lansat')


class IndicesLandsatDialog(QtWidgets.QDialog, FORM_CLASS):
    def __init__(self, parent=None):
        """Constructor."""
        super(IndicesLandsatDialog, self).__init__(parent)
        # Set up the user interface from Designer through FORM_CLASS.
        # After self.setupUi() you can access any designer object by doing
        # self.<objectname>, and you can use autoconnect slots - see
        # http://qt-project.org/doc/qt-4.8/designer-using-a-ui-file.html
        # #widgets-and-dialogs-with-auto-connect
        self.setupUi(self)
  • FORM_CLASS, _ = uic.loadUiType(...): carga indices_lansat_dialog_base.ui y genera la clase con el diseño.
  • class IndicesLandsatDialog(QtWidgets.QDialog, FORM_CLASS):: herencia múltiple: ventana de diálogo más el diseño del .ui.
  • self.setupUi(self): construye los elementos y los vuelve atributos (self.checkBox_ndvi, self.lineEdit_b4...).

Sección 5 · indices_lansat_dialog_base.ui: el diseño de la ventana

El creador eliminó los botones OK/Cancel de la plantilla y diseñó en Qt Designer una ventana de 503 × 408 píxeles (franja azul). Es la más completa de la serie.

indices_lansat_dialog_base.ui
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>IndicesLandsatDialogBase</class>
 <widget class="QDialog" name="IndicesLandsatDialogBase">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>503</width>
    <height>408</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Índices Landsat</string>
  </property>
  <widget class="QGroupBox" name="groupBox_indices">
   <property name="geometry">
    <rect>
     <x>20</x>
     <y>20</y>
     <width>471</width>
     <height>61</height>
    </rect>
   </property>
   <property name="title">
    <string>Seleccione los índices a calcular</string>
   </property>
   <widget class="QCheckBox" name="checkBox_ndvi">
    <property name="geometry">
     <rect>
      <x>30</x>
      <y>30</y>
      <width>64</width>
      <height>18</height>
     </rect>
    </property>
    <property name="text">
     <string>NDVI</string>
    </property>
   </widget>
   <widget class="QCheckBox" name="checkBox_savi">
    <property name="geometry">
     <rect>
      <x>140</x>
      <y>30</y>
      <width>64</width>
      <height>18</height>
     </rect>
    </property>
    <property name="text">
     <string>SAVI</string>
    </property>
   </widget>
   <widget class="QCheckBox" name="checkBox_ndmi">
    <property name="geometry">
     <rect>
      <x>260</x>
      <y>30</y>
      <width>64</width>
      <height>18</height>
     </rect>
    </property>
    <property name="text">
     <string>NDMI</string>
    </property>
   </widget>
   <widget class="QCheckBox" name="checkBox_nbr">
    <property name="geometry">
     <rect>
      <x>390</x>
      <y>30</y>
      <width>64</width>
      <height>18</height>
     </rect>
    </property>
    <property name="text">
     <string>NBR</string>
    </property>
   </widget>
  </widget>
  <widget class="QLineEdit" name="lineEdit_b4">
   <property name="enabled">
    <bool>false</bool>
   </property>
   <property name="geometry">
    <rect>
     <x>90</x>
     <y>110</y>
     <width>361</width>
     <height>20</height>
    </rect>
   </property>
   <property name="text">
    <string/>
   </property>
  </widget>
  <widget class="QLineEdit" name="lineEdit_b5">
   <property name="enabled">
    <bool>false</bool>
   </property>
   <property name="geometry">
    <rect>
     <x>90</x>
     <y>140</y>
     <width>361</width>
     <height>20</height>
    </rect>
   </property>
  </widget>
  <widget class="QLineEdit" name="lineEdit_b6">
   <property name="enabled">
    <bool>false</bool>
   </property>
   <property name="geometry">
    <rect>
     <x>90</x>
     <y>170</y>
     <width>361</width>
     <height>20</height>
    </rect>
   </property>
  </widget>
  <widget class="QLineEdit" name="lineEdit_b7">
   <property name="enabled">
    <bool>false</bool>
   </property>
   <property name="geometry">
    <rect>
     <x>90</x>
     <y>200</y>
     <width>361</width>
     <height>20</height>
    </rect>
   </property>
  </widget>
  <widget class="QLabel" name="label_b4">
   <property name="enabled">
    <bool>false</bool>
   </property>
   <property name="geometry">
    <rect>
     <x>30</x>
     <y>110</y>
     <width>47</width>
     <height>14</height>
    </rect>
   </property>
   <property name="text">
    <string>Banda 4</string>
   </property>
  </widget>
  <widget class="QLabel" name="label_b5">
   <property name="enabled">
    <bool>false</bool>
   </property>
   <property name="geometry">
    <rect>
     <x>30</x>
     <y>140</y>
     <width>47</width>
     <height>14</height>
    </rect>
   </property>
   <property name="text">
    <string>Banda 5</string>
   </property>
  </widget>
  <widget class="QLabel" name="label_b6">
   <property name="enabled">
    <bool>false</bool>
   </property>
   <property name="geometry">
    <rect>
     <x>30</x>
     <y>170</y>
     <width>47</width>
     <height>14</height>
    </rect>
   </property>
   <property name="text">
    <string>Banda 6</string>
   </property>
  </widget>
  <widget class="QLabel" name="label_b7">
   <property name="enabled">
    <bool>false</bool>
   </property>
   <property name="geometry">
    <rect>
     <x>30</x>
     <y>200</y>
     <width>47</width>
     <height>14</height>
    </rect>
   </property>
   <property name="text">
    <string>Banda 7</string>
   </property>
  </widget>
  <widget class="QPushButton" name="pushButton_b4">
   <property name="enabled">
    <bool>false</bool>
   </property>
   <property name="geometry">
    <rect>
     <x>460</x>
     <y>110</y>
     <width>31</width>
     <height>23</height>
    </rect>
   </property>
   <property name="text">
    <string>...</string>
   </property>
  </widget>
  <widget class="QPushButton" name="pushButton_b5">
   <property name="enabled">
    <bool>false</bool>
   </property>
   <property name="geometry">
    <rect>
     <x>460</x>
     <y>140</y>
     <width>31</width>
     <height>23</height>
    </rect>
   </property>
   <property name="text">
    <string>...</string>
   </property>
  </widget>
  <widget class="QPushButton" name="pushButton_b6">
   <property name="enabled">
    <bool>false</bool>
   </property>
   <property name="geometry">
    <rect>
     <x>460</x>
     <y>170</y>
     <width>31</width>
     <height>23</height>
    </rect>
   </property>
   <property name="text">
    <string>...</string>
   </property>
  </widget>
  <widget class="QPushButton" name="pushButton_b7">
   <property name="enabled">
    <bool>false</bool>
   </property>
   <property name="geometry">
    <rect>
     <x>460</x>
     <y>200</y>
     <width>31</width>
     <height>23</height>
    </rect>
   </property>
   <property name="text">
    <string>...</string>
   </property>
  </widget>
  <widget class="QPushButton" name="pushButton_salidas">
   <property name="geometry">
    <rect>
     <x>460</x>
     <y>300</y>
     <width>31</width>
     <height>23</height>
    </rect>
   </property>
   <property name="text">
    <string>...</string>
   </property>
  </widget>
  <widget class="QLabel" name="label_prefijo">
   <property name="geometry">
    <rect>
     <x>30</x>
     <y>260</y>
     <width>111</width>
     <height>16</height>
    </rect>
   </property>
   <property name="text">
    <string>Prefijo para la salidas</string>
   </property>
  </widget>
  <widget class="QLabel" name="label_salidas">
   <property name="geometry">
    <rect>
     <x>30</x>
     <y>300</y>
     <width>81</width>
     <height>16</height>
    </rect>
   </property>
   <property name="text">
    <string>Carpeta salidas</string>
   </property>
  </widget>
  <widget class="QLineEdit" name="lineEdit_prefijo">
   <property name="geometry">
    <rect>
     <x>152</x>
     <y>260</y>
     <width>121</width>
     <height>20</height>
    </rect>
   </property>
  </widget>
  <widget class="QLineEdit" name="lineEdit_salidas">
   <property name="geometry">
    <rect>
     <x>120</x>
     <y>300</y>
     <width>331</width>
     <height>20</height>
    </rect>
   </property>
  </widget>
  <widget class="QPushButton" name="pushButton_aceptar">
   <property name="geometry">
    <rect>
     <x>140</x>
     <y>350</y>
     <width>75</width>
     <height>23</height>
    </rect>
   </property>
   <property name="text">
    <string>Aceptar</string>
   </property>
  </widget>
  <widget class="QPushButton" name="pushButton_cancelar">
   <property name="geometry">
    <rect>
     <x>280</x>
     <y>350</y>
     <width>75</width>
     <height>23</height>
    </rect>
   </property>
   <property name="text">
    <string>Cancelar</string>
   </property>
  </widget>
 </widget>
 <resources/>
 <connections/>
</ui>
  • <widget class="QDialog" name="IndicesLandsatDialogBase">: la ventana; su windowTitle es personalizado ("Índices Landsat").
  • QGroupBox groupBox_indices: un marco con el título "Seleccione los índices a calcular". Es un contenedor: las cuatro casillas están dentro de él.
  • QCheckBox (checkBox_ndvi, checkBox_savi, checkBox_ndmi, checkBox_nbr): una casilla por índice. Python lee si están marcadas con isChecked().
  • QLabel, QLineEdit y QPushButton de las bandas 4 a 7 (label_b4, lineEdit_b4, pushButton_b4...): cada uno tiene la propiedad enabled en false, por eso la ventana abre con los campos de las bandas deshabilitados; el código los habilita según las casillas.
  • lineEdit_prefijo y lineEdit_salidas con su botón pushButton_salidas: el prefijo para los nombres de las salidas y la carpeta donde se guardan. Siempre están habilitados.
  • pushButton_aceptar y pushButton_cancelar: ejecutan el cálculo y cierran la ventana.
  • <connections/>: vacío; todas las señales se conectan con Python en run.
  • La convención de nombres es tipo_contenido (checkBox_, lineEdit_, pushButton_, label_) y el número de banda en el sufijo hace posible leerlos con facilidad en el código.

Sección 6 · pb_tool.cfg: configuración para desplegar

Lo usa pb_tool para copiar el complemento a la carpeta de complementos de QGIS con pb_tool deploy.

pb_tool.cfg
#/***************************************************************************
# IndicesLandsat
#
# Configuration file for plugin builder tool (pb_tool)
# Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
#                            -------------------
#       begin               : 2026-10-02
#       copyright           : (C) 2026 by UAEM
#       email               : indiceslandsat@uaemex.mx
# ***************************************************************************/
#
#/***************************************************************************
# *                                                                         *
# *   This program is free software; you can redistribute it and/or modify  *
# *   it under the terms of the GNU General Public License as published by  *
# *   the Free Software Foundation; either version 2 of the License, or     *
# *   (at your option) any later version.                                   *
# *                                                                         *
# ***************************************************************************/
# 
# 
# You can install pb_tool using:
#  pip install http://geoapt.net/files/pb_tool.zip
# 
# Consider doing your development (and install of pb_tool) in a virtualenv.
# 
# For details on setting up and using pb_tool, see:
#  http://g-sherman.github.io/plugin_build_tool/
#
# Issues and pull requests here:
# https://github.com/g-sherman/plugin_build_tool:
# 
# Sane defaults for your plugin generated by the Plugin Builder are
# already set below.
# 
# As you add Python source files and UI files to your plugin, add
# them to the appropriate [files] section below.

[plugin]
# Name of the plugin. This is the name of the directory that will
# be created in .qgis2/python/plugins
name: indices_lansat

# Full path to where you want your plugin directory copied. If empty,
# the QGIS default path will be used. Don't include the plugin name in
# the path.
plugin_path: C:/Users/rico_/AppData/Roaming/QGIS/QGIS3/profiles/default/python/plugins

[files]
# Python  files that should be deployed with the plugin
python_files: __init__.py indices_lansat.py indices_lansat_dialog.py

# The main dialog file that is loaded (not compiled)
main_dialog: indices_lansat_dialog_base.ui

# Other ui files for dialogs you create (these will be compiled)
compiled_ui_files: 

# Other files required for the plugin
extras: metadata.txt icon.png

# Other directories to be deployed with the plugin.
# These must be subdirectories under the plugin directory
extra_dirs:

# ISO code(s) for any locales (translations), separated by spaces.
# Corresponding .ts files must exist in the i18n directory
locales:

#Resource files
resource_files:

[help]
# the built help directory that should be deployed with the plugin
dir: help/build/html
# the name of the directory to target in the deployed plugin 
target: help
  • name: indices_lansat: nombre de la carpeta que se crea al desplegar. Personalizado (módulo del formulario).
  • plugin_path: carpeta de complementos del perfil de QGIS del usuario.
  • python_files y main_dialog: archivos que se copian. Personalizados.
  • extras: archivos adicionales (metadata.txt, icon.png).

Resumen: lo que personalizó el creador

  • Franja amarilla (formulario de Plugin Builder): el nombre de la clase (IndicesLandsat) y del módulo (indices_lansat), el nombre del complemento ("Índices Landsat"), la descripción, la autoría, el correo, las direcciones web y las etiquetas.
  • Franja azul (programado a mano): las importaciones adicionales, los métodos que controlan la ventana (cancelar, seleccionar_ndvi_savi, seleccionar_ndmi, seleccionar_nbr), los de selección de archivos (capa_b4 a capa_b7 y carpeta_salidas), el cálculo (aceptar), la conexión de señales en run y el diseño completo de la ventana.
  • Síntesis de la serie: este complemento reúne el trabajo de los ejercicios 4, 5 y 6 (los índices espectrales) y de los ejercicios 9 y 10 (la estructura de un complemento con ventana propia).
Pon a prueba lo analizado
Volver a PASIG II