Escribir Scripts Python para el Marco de Procesamiento (QGIS3)

Uno puede escribir scripts pyqgis autónomos que pueden ser ejecutados mediante la Consola de Python en QGIS. Con unos pocos ajustes, puede hacer que sus scripts autónomos se ejecuten mediante el Marco de Procesamiento. Esto tiene varias ventajas. Primero, es mucho más fácil tomar entradas del usuario y escribir archivos de salida debido a que el Marco de Procesamiento ofrece un interfaz de usuario estandarizado para estos. Segundo, el tener su script en la Caja de Herramientas de Procesamiento también le permite que sea parte de cualquier Modelo de Procesamiento o que sea ejecutado como un Trabajo por Lotes con múltiples entradas. Este tutorial mostrará cómo escribir un script python personalizado que puede ser parte del Marco de Procesamiento en QGIS.

Nota

La API Procesamiento fue completamente revisada en QGIS3. Por favor refiérase a esta guía para las mejores prácticas y consejos.

Vista general de la tarea

Nuestro script realizará una operación de disolución basada en un campo escogido por el usuario. También sumará valores de otro campo para las entidades disueltas. En el ejemplo, disolveremos un archivo shape del mundo basado en un atributo CONTINENT y sumaremos el campo POP_EST para calcular la población total en la región disuelta.

Obtener los datos

Usaremos el conjunto de datos Admin 0 - Countries de Natural Earth.

Descargue el archivo shape Admin 0 - countries..

Fuente de Datos [NATURALEARTH]

Para su comodidad, puede descargar directamente un geopackage que contiene las capas de arriba de aquí abajo:

ne_global.gpkg

Procedimiento

  1. En el Panel Navegador QGIS, localice el directorio donde guardó sus datos descargados. Expanda el zip o la entrada gpkg y seleccione la capa ne_10m_admin_0_countries. Arrastre la capa a la pantalla.

../../_images/1129.png
  1. Vaya a Procesos ‣ Caja de Herramientas. Clic en el botón Scripts en la barra de herramientas y seleccione Crear Nuevo Script de Plantilla.

../../_images/290.png
  1. La plantilla contiene todo el código repetitivo que es requerido para que el Marco Procesamiento lo reconozca como un script Procesamiento, y gestione entradas/salidas. Comencemos personalizando la plantilla ejemplo a nuestras necesidades. Primero cambie el nombre de clase de ExampleProcessingAlgorithm a DissolveProcessingAlgorithm. Este nombre también necesita ser actualizado en el método createInstance. Agregue una dosctring a la clase que explica lo que hace el algoritmo.

../../_images/345.png
  1. A medida que se desliza hacia abajo, verá métodos que asignan nombre, grupo, descripción, etc. al script. cambie los valores de retorno para el método name para que sea dissolve_with_sum, el método displayName a Dissolve with Sum, el método group y el método groupId a scripts. Cambie el valor de retorno del método shortHelpString a una descripción que aparecerá al usuario. Clic en el botón Guardar.

../../_images/426.png
  1. Nombre el script como dissolve_with_sum y guárdelo en la ubicación predeterminada bajo la carpeta perfiles ‣ predeterminado ‣ procesos ‣ scripts.

../../_images/526.png
  1. Ahora definiremos las entradas para el script. La plantilla ya contiene una definición de un Capa Vector INPUT y una capa OUTPUT. Agregaremos 2 nuevas entradas que permiten al usuario seleccionar un DISSOLVE_FIELD y un SUM_FIELD. Agregue un nueva importación arriba y el siguiente código en el método initAlgorithm. Clic en el botón Ejecutar para previsualizar los cambios.

from qgis.core import QgsProcessingParameterField

self.addParameter(
        QgsProcessingParameterField(
                self.DISSOLVE_FIELD,
                'Choose Dissolve Field',
                '',
                self.INPUT))

self.addParameter(
        QgsProcessingParameterField(
                self.SUM_FIELD,
                'Choose Sum Field',
                '',
                self.INPUT))
../../_images/6a.png ../../_images/6b.png
  1. Verá un diálogo Disolver con Suma con nuestras entradas recientemente definidas. Seleccione la capa ne_10m_admin_0_countries como la Capa de entrada`. Como tanto Campo disolución como Campos Suma son filtrados en base a la capa de entrada, ellos serán pre-poblados con campos existentes de la capa de entrada. Clic en el botón Cerrar.

../../_images/725.png
  1. Ahora definiremos nuestra lógica personalizada para procesar datos en el método processAlgorithm. A este método se le pasa un diccionario lamado parameters. Contiene las entradas que el usuario tiene seleccionadas. Hay métodos que ayudan que le permiten tomar estas entradas y crear objetos apropiados. Primero conseguimos nuestras entradas usando los métodos parameterAsSource y parameterAsString. A continuación, queremos crear un sumidero de entidades donde escribiremos la salida. QGIS3 tiene una nueva clase llamada QgsFeatureSink que es la manera preferida para crear objetos que puedan aceptar nuevas entidades. La salida necesita sólo 2 campos - uno para el valor del campo disuelto y otro para la suma del campo seleccionado.

from PyQt5.QtCore import QVariant
from qgis.core import QgsField, QgsFields

source = self.parameterAsSource(
        parameters,
        self.INPUT,
        context)
dissolve_field = self.parameterAsString(
        parameters,
        self.DISSOLVE_FIELD,
        context)
sum_field = self.parameterAsString(
        parameters,
        self.SUM_FIELD,
        context)

fields = QgsFields()
fields.append(QgsField(dissolve_field, QVariant.String))
fields.append(QgsField('SUM_' + sum_field, QVariant.Double))

(sink, dest_id) = self.parameterAsSink(
        parameters,
        self.OUTPUT,
        context, fields, source.wkbType(), source.sourceCrs())
../../_images/8a.png ../../_images/8b.png
  1. Ahora alistaremos los objetos de entrada y crearemos un diccionario para retener los valores únicos del dissolve_field y la suma de los valores del sum_field. Note el uso del método feedback.pushInfo() para comunicar el estado con el usuario.

feedback.pushInfo('Extracting unique values from dissolve_field and computing sum')

features = source.getFeatures()
unique_values = set(f[dissolve_field] for f in features)

# Get Indices of dissolve field and sum field
dissolveIdx = source.fields().indexFromName(dissolve_field)
sumIdx = source.fields().indexFromName(sum_field)

# Find all unique values for the given dissolve_field and
# sum the corresponding values from the sum_field
sum_unique_values = {}
attrs = [{dissolve_field: f[dissolveIdx], sum_field: f[sumIdx]} for f in source.getFeatures()]
for unique_value in unique_values:
        val_list = [ f_attr[sum_field] for f_attr in attrs if f_attr[dissolve_field] == unique_value]
        sum_unique_values[unique_value] = sum(val_list)
../../_images/925.png
  1. A continuación, llamaremos el algoritmo incorporado de procesamiento native:dissolve sobre la capa de entrada para generar las geometrías disueltas. Una vez que tengamos las geometrías disueltas, iteraremos a través de la salida del algoritmo de disolución y crearemos nuevos objetos para ser agregados a la salida. Al final regresaremos el dest_id FeatureSink como la salida. Ahora el script está listo. Clic en el botón Ejecutar.

Nota

Note el uso de parameters[self.INPUT] para ir a buscar la capa de entrada del diccionario de parámetros directamente sin definirlo como una fuente. Como estamos pasando el objeto de entrada al algoritmo sin hacer nada con él, no es necesario definirlo como una fuente.

from qgis.core import QgsFeature

# Running the processing dissolve algorithm
feedback.pushInfo('Dissolving features')
dissolved_layer = processing.run("native:dissolve", {
        'INPUT': parameters[self.INPUT],
        'FIELD': dissolve_field,
        'OUTPUT': 'memory:'
        }, context=context, feedback=feedback)['OUTPUT']

# Read the dissolved layer and create output features
for f in dissolved_layer.getFeatures():
        new_feature =  QgsFeature()
        # Set geometry to dissolved geometry
        new_feature.setGeometry(f.geometry())
        # Set attributes from sum_unique_values dictionary that we had computed
        new_feature.setAttributes([f[dissolve_field], sum_unique_values[f[dissolve_field]]])
        sink.addFeature(new_feature, QgsFeatureSink.FastInsert)

return {self.OUTPUT: dest_id}
../../_images/10a.png ../../_images/10b.png
  1. En el diálogo Disolver con Suma, seleccione ne_10m_admin_0_countries como la Capa de entrada, CONTINENT como el Campo de disolución y POP_EST como el Campo de suma. Clic en Ejecutar.

../../_images/1130.png
  1. Una vez finalice el procesamiento, haga clic en el botón Cerrar y cámbiese a la ventana principal de QGIS.

../../_images/1227.png
  1. Verá la capa de salida disuelta con un objeto espacial por cada continente y la población total agregada de los países individuales que pertenecen a ese continente.

../../_images/1326.png
  1. Otra ventaja de escribir scripts de procesamiento es que los métodos dentro del Marco de Procesamiento identifican la sección de capa y automáticamente filtran sus entrada para usar sólo los objetos seleccionados. Esto sucede porque estamos definiendo nuestra entrada como una QgsProcessingParameterFeatureSource. Una fuente de objeto permite usar CUALQUIER objeto que contiene objetos vector, no sólo una capa vector, por lo que cuando hay objetos seleccionados en su capa y se pide a Procesamiento usar objetos seleccionados, la entrada es pasada a su script como un objeto QgsProcessingFeatureSource que contiene los objetos seleccionados y no la capa vector completa. Aquí tiene una demostración rápida de esta funcionalidad. Digamos que queremos disolver sólo ciertos continentes. Creemos una selección usando la herramienta Seleccionar objeto por Expresión.

../../_images/1424.png
  1. Ingrese la siguiente expresión para seleccionar objetos de Norte y Sud América y clic en Seleccionar.

"CONTINENT" = 'North America' OR "CONTINENT" = 'South America'
../../_images/1524.png
  1. Verá que los objetos seleccionados son resaltados en amarillo. Localice el script dissolve_with_sum y haga doble-clic para ejecutarlo.

../../_images/1623.png
  1. En el diálogo Disolver con Suma, seleccione ne_10m_admin_0_countries como la Capa de entrada. Esta vez, asegúrese que marca la casilla Sólo objetos seleccionados. Elija SUBREGIÓN como el Campo disolver y POP_EST como el Campo suma.

../../_images/1724.png
  1. Una vez que termine el proceso, clic en Cerrar y vuelva a la ventana principal QGIS. Notará una nueva capa con sólo los objetos seleccionados disueltos. Clic en el botón Identificar y clic en un objeto para inspeccionar y verificar que el script funcionó correctamente.

../../_images/1821.png

Abajo está el script completo para referencia. Puede modificarlo para ajustarlo a sus necesidades.

# -*- coding: utf-8 -*-

"""
***************************************************************************
*                                                                         *
*   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 PyQt5.QtCore import QCoreApplication, QVariant
from qgis.core import (QgsProcessing,
                       QgsFeatureSink,
                       QgsFeature,
                       QgsField,
                       QgsFields,
                       QgsProcessingException,
                       QgsProcessingAlgorithm,
                       QgsProcessingParameterFeatureSource,
                       QgsProcessingParameterFeatureSink,
                       QgsProcessingParameterField,
                       )
import processing


class DissolveProcessingAlgorithm(QgsProcessingAlgorithm):
    """
    Dissolve algorithm that dissolves features based on selected
    attribute and summarizes the selected field by cumputing the
    sum of dissolved features.
    """
    INPUT = 'INPUT'
    OUTPUT = 'OUTPUT'
    DISSOLVE_FIELD = 'dissolve_field'
    SUM_FIELD = 'sum_field'

    def tr(self, string):
        """
        Returns a translatable string with the self.tr() function.
        """
        return QCoreApplication.translate('Processing', string)

    def createInstance(self):
        return DissolveProcessingAlgorithm()

    def name(self):
        """
        Returns the algorithm name, used for identifying the algorithm. This
        string should be fixed for the algorithm, and must not be localised.
        The name should be unique within each provider. Names should contain
        lowercase alphanumeric characters only and no spaces or other
        formatting characters.
        """
        return 'dissolve_with_sum'

    def displayName(self):
        """
        Returns the translated algorithm name, which should be used for any
        user-visible display of the algorithm name.
        """
        return self.tr('Dissolve with Sum')

    def group(self):
        """
        Returns the name of the group this algorithm belongs to. This string
        should be localised.
        """
        return self.tr('scripts')

    def groupId(self):
        """
        Returns the unique ID of the group this algorithm belongs to. This
        string should be fixed for the algorithm, and must not be localised.
        The group id should be unique within each provider. Group id should
        contain lowercase alphanumeric characters only and no spaces or other
        formatting characters.
        """
        return 'scripts'

    def shortHelpString(self):
        """
        Returns a localised short helper string for the algorithm. This string
        should provide a basic description about what the algorithm does and the
        parameters and outputs associated with it..
        """
        return self.tr("Dissolves selected features and creates and sums values of features that were dissolved")

    def initAlgorithm(self, config=None):
        """
        Here we define the inputs and output of the algorithm, along
        with some other properties.
        """
        # We add the input vector features source. It can have any kind of
        # geometry.
        self.addParameter(
            QgsProcessingParameterFeatureSource(
                self.INPUT,
                self.tr('Input layer'),
                [QgsProcessing.TypeVectorAnyGeometry]
            )
        )
        self.addParameter(
            QgsProcessingParameterField(
                self.DISSOLVE_FIELD,
                'Choose Dissolve Field',
                '',
                self.INPUT))
        self.addParameter(
            QgsProcessingParameterField(
                self.SUM_FIELD,
                'Choose Sum Field',
                '',
                self.INPUT))
        # We add a feature sink in which to store our processed features (this
        # usually takes the form of a newly created vector layer when the
        # algorithm is run in QGIS).
        self.addParameter(
            QgsProcessingParameterFeatureSink(
                self.OUTPUT,
                self.tr('Output layer')
            )
        )

    def processAlgorithm(self, parameters, context, feedback):
        """
        Here is where the processing itself takes place.
        """
        source = self.parameterAsSource(
            parameters,
            self.INPUT,
            context
        )
        dissolve_field = self.parameterAsString(
            parameters,
            self.DISSOLVE_FIELD,
            context)
        sum_field = self.parameterAsString(
            parameters,
            self.SUM_FIELD,
            context)
        
        fields = QgsFields()
        fields.append(QgsField(dissolve_field, QVariant.String))
        fields.append(QgsField('SUM_' + sum_field, QVariant.Double))
        
        (sink, dest_id) = self.parameterAsSink(
            parameters,
            self.OUTPUT,
            context, fields, source.wkbType(), source.sourceCrs())
        
        # Create a dictionary to hold the unique values from the 
        # dissolve_field and the sum of the values from the sum_field
        feedback.pushInfo('Extracting unique values from dissolve_field and computing sum')
        features = source.getFeatures()
        unique_values = set(f[dissolve_field] for f in features)
        # Get Indices of dissolve field and sum field
        dissolveIdx = source.fields().indexFromName(dissolve_field)
        sumIdx = source.fields().indexFromName(sum_field)
        
        # Find all unique values for the given dissolve_field and
        # sum the corresponding values from the sum_field
        sum_unique_values = {}
        attrs = [{dissolve_field: f[dissolveIdx], sum_field: f[sumIdx]}
                for f in source.getFeatures()]
        for unique_value in unique_values:
            val_list = [ f_attr[sum_field] 
                for f_attr in attrs if f_attr[dissolve_field] == unique_value]
            sum_unique_values[unique_value] = sum(val_list)
        
        # Running the processing dissolve algorithm
        feedback.pushInfo('Dissolving features')
        dissolved_layer = processing.run("native:dissolve", {
            'INPUT': parameters[self.INPUT],
            'FIELD': dissolve_field,
            'OUTPUT': 'memory:'
        }, context=context, feedback=feedback)['OUTPUT']
        
        # Read the dissolved layer and create output features
        for f in dissolved_layer.getFeatures():
            new_feature =  QgsFeature()
            # Set geometry to dissolved geometry
            new_feature.setGeometry(f.geometry())
            # Set attributes from sum_unique_values dictionary that we had computed
            new_feature.setAttributes([f[dissolve_field], sum_unique_values[f[dissolve_field]]])
            sink.addFeature(new_feature, QgsFeatureSink.FastInsert)
        
        return {self.OUTPUT: dest_id}
comments powered by Disqus