3

I am working on a PyQGIS script that gives similar behaviour to the GeoCube package. Specifically, I want to create a bunch of rasters from vector data for a selection of fields.

For example, if I have a DEM and a soil vector layer with data for bulk density, cohesion etc. I want to make a bunch of rasters that are the same as the DEM in terms of extent and cell size, but then have the corresponding values of the vector layer. I made the following script, first in modelbuilder and then extended in Python. The main problem now, is that all output rasters are called "Rasterized".

A bunch of "rasterized" rastters

"""
Model exported as python.
Name : Batch_raster
Group : 
With QGIS : 31611
"""

from qgis.core import QgsProcessing from qgis.core import QgsProcessingAlgorithm from qgis.core import QgsProcessingMultiStepFeedback from qgis.core import QgsProcessingParameterRasterLayer from qgis.core import QgsProcessingParameterVectorLayer from qgis.core import QgsProcessingParameterField from qgis.core import QgsProcessingParameterBoolean from qgis.core import QgsProcessingParameterRasterDestination from qgis.core import QgsProcessingLayerPostProcessorInterface import processing

class Batch_raster(QgsProcessingAlgorithm): INPUT = 'INPUT' FIELDS = 'FIELDS'

def initAlgorithm(self, config=None):
    self.addParameter(QgsProcessingParameterRasterLayer('likeraster', 'like raster', defaultValue=None))
    self.addParameter(QgsProcessingParameterVectorLayer(self.INPUT, 'vector', defaultValue=None))
    self.addParameter(QgsProcessingParameterField(self.FIELDS, 'fields to select', allowMultiple=True, defaultToAllFields=True, parentLayerParameterName=self.INPUT))
    self.addParameter(QgsProcessingParameterBoolean('VERBOSE_LOG', 'Verbose logging', optional=True, defaultValue=False))
    self.addParameter(QgsProcessingParameterRasterDestination('Raster_out', 'raster_out', createByDefault=True, defaultValue=None))

def processAlgorithm(self, parameters, context, model_feedback):
    # Use a multi-step feedback, so that individual child algorithm progress reports are adjusted for the
    # overall progress through the model
    feedback = QgsProcessingMultiStepFeedback(1, model_feedback)
    results = {}
    outputs = {}

    likeraster = self.parameterAsRasterLayer(parameters, 'likeraster', context)
    vector = self.parameterAsVectorLayer(parameters, self.INPUT, context)
    fields = self.parameterAsFields(parameters, self.FIELDS, context)

    for field in fields:
        feedback.pushInfo(str(field))

        # Rasterize (vector to raster)
        alg_params = {
            'BURN': 0,
            'DATA_TYPE': 5,
            'EXTENT': likeraster.extent(),
            'EXTRA': '',
            'FIELD': field,
            'INIT': None,
            'INPUT': parameters[self.INPUT],
            'INVERT': False,
            'NODATA': 0,
            'OPTIONS': '',
            'UNITS': 1,
            'OUTPUT': f'{field}',
            'HEIGHT':likeraster.rasterUnitsPerPixelY(),
            'WIDTH': likeraster.rasterUnitsPerPixelX(),
            'OUTPUT': parameters['Raster_out'],
        }



        outputs[f'RasterizeVectorToRaster{field}'] = processing.run('gdal:rasterize', alg_params, context=context, feedback=feedback)#, is_child_algorithm=True)
    #processing.run('gdal:rasterize',alg_params, 
    #global renamer
    #renamer = Renamer(field)
    #context.layerToLoadOnCompletionDetails(outputs[f'RasterizeVectorToRaster{field}']).setPostProcessor(renamer)

    results[f'Raster_out{field}'] = outputs[f'RasterizeVectorToRaster{field}']['OUTPUT']
    return results

def name(self):
    return 'Batch_raster'

def displayName(self):
    return 'Batch_raster'

def group(self):
    return ''

def groupId(self):
    return ''

def createInstance(self):
    return Batch_raster()

I have tried many things, among which this post-processing class and naming it different outputs, all with no avail.

Fee
  • 1,158
  • 4
  • 14
  • 1
    As per the [help/behavior] please do not include chit chat like statements of appreciation within your posts. – PolyGeo Oct 22 '21 at 08:42

1 Answers1

2

Update: Some 16 months later I know slightly more, so am adding an improved solution.

Loading layers inside the postProcessAlgorithm() method (as per my original answer) is not best practice nor is it required for this task. You can achieve this by using a boolean parameter to check if the resulting layers, from either a temporary or file folder, should be loaded on completion. If the parameter evaluates to True, add the resulting layers to the context object's temporary layer store and layersToLoadOnCompletion(), passing a QgsProcessingContext.LayerDetails object, where you can set the layer's display name. This also allows processing to cleanly handle the loading of the output layers.

from qgis.PyQt.QtCore import QDir
from qgis.core import (QgsProcessing,
                        QgsProcessingAlgorithm,
                        QgsProcessingParameterRasterLayer,
                        QgsProcessingParameterVectorLayer,
                        QgsProcessingParameterField,
                        QgsProcessingParameterBoolean,
                        QgsProcessingParameterFolderDestination,
                        QgsProcessingMultiStepFeedback,
                        QgsProcessingContext,
                        QgsProject,
                        QgsRasterLayer,
                        QgsProcessingOutputMultipleLayers)

from pathlib import Path
import processing
import os

class Batch_raster(QgsProcessingAlgorithm):
    INPUT = 'INPUT'
    FIELDS = 'FIELDS'
    OUT_FOLDER = 'OUT_FOLDER'
    LOAD_OUTPUTS = 'LOAD_OUTPUTS'
    OUTPUT_LAYERS = 'OUTPUT_LAYERS'

    def initAlgorithm(self, config=None):
        self.addParameter(QgsProcessingParameterRasterLayer('likeraster', 'like raster', defaultValue=None))
        self.addParameter(QgsProcessingParameterVectorLayer(self.INPUT, 'vector', defaultValue=None))
        self.addParameter(QgsProcessingParameterField(self.FIELDS, 'fields to select', type=0, allowMultiple=True, defaultToAllFields=True, parentLayerParameterName=self.INPUT))
        self.addParameter(QgsProcessingParameterBoolean('VERBOSE_LOG', 'Verbose logging', optional=True, defaultValue=False))
        self.addParameter(QgsProcessingParameterFolderDestination(self.OUT_FOLDER, 'Output directory')),
        self.addParameter(QgsProcessingParameterBoolean(self.LOAD_OUTPUTS, 'Load output layers?', optional=True, defaultValue=True))
        self.addOutput(QgsProcessingOutputMultipleLayers(self.OUTPUT_LAYERS, 'Output layers'))

    def processAlgorithm(self, parameters, context, model_feedback):
        # Use a multi-step feedback, so that individual child algorithm progress reports are adjusted for the
        # overall progress through the model
        results = {}
        outputs = {}
        output_layers = []

        likeraster = self.parameterAsRasterLayer(parameters, 'likeraster', context)
        vector = self.parameterAsVectorLayer(parameters, self.INPUT, context)
        fields = self.parameterAsFields(parameters, self.FIELDS, context)
        out_dir = self.parameterAsString(parameters, self.OUT_FOLDER, context)
        load_outputs = self.parameterAsBool(parameters, 'LOAD_OUTPUTS', context)

        steps = len(fields)
        step = 1
        feedback = QgsProcessingMultiStepFeedback(steps, model_feedback)

        for index, field in enumerate(fields):
            if feedback.isCanceled():
                break
            feedback.pushInfo(str(field))

            if not QDir().mkpath(out_dir):
                raise QgsProcessingException('Failed to create output directory')
                return {}
            out_path = os.path.join(out_dir, f'{field}_rasterized.tif')

            # Rasterize (vector to raster)
            alg_params = {
                'BURN': 0,
                'DATA_TYPE': 5,
                'EXTENT': likeraster.extent(),
                'EXTRA': '',
                'FIELD': field,
                'INIT': None,
                'INPUT': parameters[self.INPUT],
                'INVERT': False,
                'NODATA': 0,
                'OPTIONS': '',
                'UNITS': 1,
                'HEIGHT':likeraster.rasterUnitsPerPixelY(),
                'WIDTH': likeraster.rasterUnitsPerPixelX(),
                'OUTPUT': out_path,
            }

            feedback.setCurrentStep(step)
            step+=1

            outputs[f'RasterizeVectorToRaster{field}'] = processing.run('gdal:rasterize',
                                                                        alg_params,
                                                                        is_child_algorithm=True,
                                                                        context=context,
                                                                        feedback=feedback)

            results[f'Raster_out{field}'] = outputs[f'RasterizeVectorToRaster{field}']['OUTPUT']

            output_layers.append(out_path)

            if load_outputs:
                file_name = Path(out_path).stem
                lyr = QgsRasterLayer(results[f'Raster_out{field}'], file_name, 'gdal')
                feedback.pushInfo(repr(lyr.isValid()))
                if lyr.isValid():
                    context.temporaryLayerStore().addMapLayer(lyr)
                    details = QgsProcessingContext.LayerDetails(lyr.name(), context.project(), lyr.name())
                    details.forceName = True
                    context.addLayerToLoadOnCompletion(lyr.id(), details)


        results[self.OUTPUT_LAYERS] = output_layers
        return results

    def name(self):
        return 'Batch_rasterize_fields'

    def displayName(self):
        return 'Batch_rasterize_fields'

    def group(self):
        return ''

    def groupId(self):
        return ''

    def createInstance(self):
        return Batch_raster()

Original answer:

The way I see it, you have a problem with your output destination parameter in that you are adding a QgsProcessingParameterRasterDestination which is a single raster file destination, but your processing script is creating multiple raster files in the processAlgorithm() method. So I guess that you are overwriting your output file on each iteration of calls to the rasterize child algorithm.

I think that processing algorithms which create multiple output files should use a QgsProcessingParameterFolderDestination parameter as a directory for output files.

I think this probably requires a workaround by re-implementing a postProcessAlgorithm() method in order to rename and load the output files.

I have modified your script and tested in QGIS versions 3.16 and 3.20. I am not sure if this is the best way to do this but it is at least working. I will be glad if someone knows a better way or can improve my solution.

Modified script:

import os

from qgis.core import (QgsProcessing, QgsProcessingAlgorithm, QgsProcessingParameterRasterLayer, QgsProcessingParameterVectorLayer, QgsProcessingParameterField, QgsProcessingParameterBoolean, QgsProcessingParameterFolderDestination, QgsProcessingLayerPostProcessorInterface, QgsProcessingUtils, QgsProject, QgsRasterLayer, )

import processing

class Batch_raster(QgsProcessingAlgorithm): INPUT = 'INPUT' FIELDS = 'FIELDS' OUT_FOLDER = 'OUT_FOLDER' LOAD_OUTPUTS = 'LOAD_OUTPUTS'

final_layers = {}
load_outputs = True

def initAlgorithm(self, config=None):
    self.addParameter(QgsProcessingParameterRasterLayer('likeraster', 'like raster', defaultValue=None))
    self.addParameter(QgsProcessingParameterVectorLayer(self.INPUT, 'vector', defaultValue=None))
    self.addParameter(QgsProcessingParameterField(self.FIELDS, 'fields to select', type=0, allowMultiple=True, defaultToAllFields=True, parentLayerParameterName=self.INPUT))
    self.addParameter(QgsProcessingParameterBoolean('VERBOSE_LOG', 'Verbose logging', optional=True, defaultValue=False))
    self.addParameter(QgsProcessingParameterFolderDestination(self.OUT_FOLDER, 'Output directory')),
    self.addParameter(QgsProcessingParameterBoolean(self.LOAD_OUTPUTS, 'Load output layers?', optional=True, defaultValue=True))

def processAlgorithm(self, parameters, context, feedback):
    # Use a multi-step feedback, so that individual child algorithm progress reports are adjusted for the
    # overall progress through the model
    results = {}
    outputs = {}

    likeraster = self.parameterAsRasterLayer(parameters, 'likeraster', context)
    vector = self.parameterAsVectorLayer(parameters, self.INPUT, context)
    fields = self.parameterAsFields(parameters, self.FIELDS, context)
    out_dir = self.parameterAsString(parameters, self.OUT_FOLDER, context)
    load = self.parameterAsBool(parameters, 'LOAD_OUTPUTS', context)
    if not load:
        self.load_outputs = False

    for index, field in enumerate(fields):
        if feedback.isCanceled():
            break
        feedback.pushInfo(str(field))
        if not 'OUT_FOLDER' in out_dir:
            out_path = os.path.join(out_dir, f'{field}_rasterized.tif')
        else:
            out_path = 'TEMPORARY_OUTPUT'

        # Rasterize (vector to raster)
        alg_params = {
            'BURN': 0,
            'DATA_TYPE': 5,
            'EXTENT': likeraster.extent(),
            'EXTRA': '',
            'FIELD': field,
            'INIT': None,
            'INPUT': parameters[self.INPUT],
            'INVERT': False,
            'NODATA': 0,
            'OPTIONS': '',
            'UNITS': 1,
            'OUTPUT': f'{field}',
            'HEIGHT':likeraster.rasterUnitsPerPixelY(),
            'WIDTH': likeraster.rasterUnitsPerPixelX(),
            'OUTPUT': out_path,
        }



        outputs[f'RasterizeVectorToRaster{field}'] = processing.run('gdal:rasterize',
                                                                    alg_params,
                                                                    is_child_algorithm=True,
                                                                    context=context,
                                                                    feedback=feedback)

        results[f'Raster_out{field}'] = outputs[f'RasterizeVectorToRaster{field}']['OUTPUT']
        if out_path == 'TEMPORARY_OUTPUT':
            self.final_layers[field] = QgsProcessingUtils.mapLayerFromString(outputs[f'RasterizeVectorToRaster{field}']['OUTPUT'], context)
        else:
            self.final_layers[f'file_{index}'] = QgsProcessingUtils.mapLayerFromString(outputs[f'RasterizeVectorToRaster{field}']['OUTPUT'], context)
        pcnt = int(index/len(fields)*100)
        feedback.setProgress(pcnt)
    return results

######################################################################
def postProcessAlgorithm(self, context, feedback):
    if self.load_outputs:
        for name, layer in self.final_layers.items():
            if layer.name() == 'OUTPUT':
                layer.setName(f'{name}_rasterized')
            QgsProject.instance().addMapLayer(layer)
    else:
        self.load_outputs = True
    self.final_layers.clear()
    return {}
######################################################################

def name(self):
    return 'Batch_rasterize_fields'

def displayName(self):
    return 'Batch_rasterize_fields'

def group(self):
    return ''

def groupId(self):
    return ''

def createInstance(self):
    return Batch_raster()

Results shown in short screencast below:

enter image description here

Ben W
  • 21,426
  • 3
  • 15
  • 39
  • This is so useful! thanks a lot! Also the small niceities like the status bar. Next question would be if the output layers would also be available in the graphical modeler... – Fee Oct 25 '21 at 09:19
  • 1
    @Fee, better late than never- I have updated my answer with an improved solution. – Ben W Feb 12 '23 at 06:08