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.

1
  • 1
    As per the help center please do not include chit chat like statements of appreciation within your posts.
    – PolyGeo
    Commented Oct 22, 2021 at 8:42

1 Answer 1

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

2
  • 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
    Commented Oct 25, 2021 at 9:19
  • 1
    @Fee, better late than never- I have updated my answer with an improved solution.
    – Ben W
    Commented Feb 12, 2023 at 6:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.