3

I am creating a QGIS plugin a part of which is like the SAGA Rasters sum tool. The user interface will have a widget to which the user will load the raster layers. I am trying to get the pixel-wise sum of values of the loaded rasters.

Like if the user loads 3 rasters R1, R2, R3 then for a pixel Pij, having values 5, 7 and 6 in R1, R2 and R3 respectively, in the new raster Pij = R1ij+R2ij+R3ij = 5+7+6 = 18

I am stuck up with both - creating the UI and the calculations.

So the first part of my question is about creating a QListWIdget and dragging and dropping the layers into the widget and using these layers for additions. The best I could get to was to add the QListWidget into the UI using QtCreator. Couldn't get to add and call layers from the QListWidget.

Here is the code I used with a push button to add layers in the QListWidget, but the layers add as a single row and I don't know how to use these for the actual calculations.

def add_rasters(self):
    name=QFileDialog.getOpenFileNames(self.dlg, 'Select Rasters', '','*.tif')
    name_str = str(name)
    list = []
    list.append(name)
    self.dlg.listWidget.insertItem(2,name_str)

self.dlg.pushbutton.clicked.connect(self.add_rasters)

Using the above code the ListWidget gets filled up like: (['C:/QGIS_plugin/rstr1.tif', 'C:/QGIS_plugin/rstr2.tif', 'C:/QGIS_plugin/rstr3.tif'], '*.tif')

Next I am querying about the calculations:

I can do the addition calculation by manually adding up all the layers in the list; but I could not figure out a way to do the calculation automatically for n-number of the layers loaded in the widget.

Below is my code for addition of layers already loaded in the QGIS map project.

import processing
from qgis.core import *
from PyQt5.QtCore import *
from qgis.analysis import QgsRasterCalculator, QgsRasterCalculatorEntry

entries = []

myList=[layer for layer in QgsProject.instance().mapLayers().values()]
print(my_list)

for i in range(0, len(myList)):
     raster = myList[i]
     #readRst = QgsProcessingUtils.mapLayerFromString(raster)
     ras1 = QgsRasterCalculatorEntry()
     ras1.raster = raster
     ras1.ref = "ext_lyr" + str(i+1) +"@1"
     ras1.bandNumber = 1
     entries.append(ras1)

formula = '(' + entries[0].ref + ' + ' + entries[1].ref +  ')'

output = r"C:/QGIS_plugin/rstrs_sum.tif"
calc = QgsRasterCalculator(formula, output, 'GTiff', raster.extent(), raster.width(), raster.height(), entries)
calc.processCalculation()

I am afraid that I am unable to frame this question properly (mixing two questions), but I am myself not sure how this would work best. I have gone through several posts on this site and now I am completely lost!

I am quite new to python programming and QGIS, hence need help with the simplest things also. Hoping to get some help here with this.

1 Answer 1

0

Drag & Drop

It is fairly easy to implement a QListWidget which accepts drops from the system file explorer, or the QGIS Browser.

from pathlib import Path
from PyQt5.QtWidgets import QListWidget

class DropList(QListWidget):

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setAcceptDrops(True)
        self.items = set()  # Keep a set of filepaths
        self.setAlternatingRowColors(True)
        self.setSelectionMode(QListWidget.ExtendedSelection)

    def addItem(self, item):  # Ensure that files are only added once
        if item in self.items:
            return
        self.items.add(item)
        super().addItem(str(item))

    def dragEnterEvent(self, event):
        event.acceptProposedAction()

    def dragMoveEvent(self, event):
        event.acceptProposedAction()

    def dragLeaveEvent(self, event):
        event.accept()

    def dropEvent(self, event):
        mimeData = event.mimeData()

        # Drop from QGIS Browser, or from system file explorer
        if mimeData.hasUrls():
            for url in mimeData.urls():
                filepath = Path(url.toLocalFile())
                if filepath.suffix.lower() in (".tif", ".tiff"):
                    self.addItem(filepath)

It is a bit more complicated to handle drops from the QGIS Layer panel. You have to get the qgis-specific mimeData, and recreate a QgsLayerTreeNode from it. Once you got the node, check if it matches a raster layer, and add its source to your list. dropEvent becomes:

def dropEvent(self, event):
    mimeData = event.mimeData()
    # Drop from QGIS Browser, or from system file explorer
    if mimeData.hasUrls():
        for url in mimeData.urls():
            filepath = Path(url.toLocalFile())
            if filepath.suffix.lower() in (".tif", ".tiff"):
                self.addItem(filepath)

    # Drop from QGIS Layers panel
    else:
        if not mimeData.hasFormat(  "application/qgis.layertreemodeldata"):
            return
        encodedData = mimeData.data( "application/qgis.layertreemodeldata" )
        doc = QDomDocument()
        if not doc.setContent( encodedData ):
            return
        rootElem = doc.documentElement();
        if rootElem.tagName() != "layer_tree_model_data":
            return

        elem = rootElem.firstChildElement()
        while not elem.isNull():
            node = QgsLayerTreeNode.readXml( elem, QgsProject.instance() )
            if (node and QgsLayerTree.isLayer(node) and isinstance(node.layer(), QgsRasterLayer) and node.layer().source().lower().endswith(".tif")):
                self.addItem(Path(node.layer().source()))
            elem = elem.nextSiblingElement()

Sum n rasters

Here is a simple function which takes a list of filepath, generates QRasterLayers from them, and sum them.

def sum_rasters(paths, outputname):

    # Compute entries from filenames
    layers =  [QgsRasterLayer(str(path)) for path in paths]
    entries = []
    for i, layer in enumerate(layers, 1):
        entry = QgsRasterCalculatorEntry()
        entry.ref = f"layer_{i}@1"
        entry.raster = layer
        entry.bandNumber = 1
        entries.append(entry)


    # Create operation string
    operation = "+".join(entry.ref for entry in entries)

    # Actual computation
    calc = QgsRasterCalculator(
        operation,
        outputname,
        "GTiff",
        layers[0].extent(),  # Use first layer as ref for extent & size
        layers[0].width(),
        layers[0].height(),
        entries,
    )
    return calc.processCalculation()

You may want to load the generated raster into your project afterward.

output_raster = QgsRasterLayer(outputname)
output_raster.setName("Sum")
QgsProject.instance().addMapLayer(output_raster )

Binding it together

Here is a simple dialog which uses the previously defined widgets and functions.

class ComputationDialog(QDialog):

    def __init__(self, parent=None):
        super().__init__(parent)
        layout = QVBoxLayout(self)
        self.dropList = DropList(self)

        # Optional Toolbar: add raster from file explorer, and remove raster from dialog
        self.toolbar = QToolBar(self)
        addFromFileAction = self.toolbar.addAction(
            QIcon(":/images/themes/default/mActionAdd.svg"),  # qgis icon
            "Add new raster"
        )
        addFromFileAction.triggered.connect(self.dropList.addFromFile)
        removeSelectedAction = self.toolbar.addAction(
            QIcon(":/images/themes/default/mActionRemove.svg"), # qgis icon
            "Remove selected raster"
        )
        removeSelectedAction.triggered.connect(self.dropList.removeSelected)
        self.toolbar.setIconSize(QSize(16,16))

        self.sumButton = QPushButton("Sum")
        self.sumButton.clicked.connect(self.onSum)

        layout.addWidget(self.toolbar)
        layout.addWidget(self.dropList)
        layout.addWidget(self.sumButton)


    def onSum(self):
        sum_rasters(self.dropList.items, "C:/Temp/Output.tif")

In order to use the toolbar, you have to add the following method to yout DropList Class

def addFromFile(self):
    names, _ =  QFileDialog.getOpenFileNames(self, 'Select Rasters', '','*.tif')
    for name in names:
        self.addItem(Path(name))

def removeSelected(self):
    for item in self.selectedItems():
        self.items.remove(Path(item.text()))
        self.takeItem(self.row(item))
1
  • This is amazing!!! Thank you so very much! I could load the layers in the widget using the addFromFile() method and do the calculations using sum_rasters(). I am still trying the drag and drop option. All this while I was trying to understand and implement this code. Hence the delay in accepting this answer. But really thanks a lot!
    – D_lind
    Dec 8, 2019 at 19:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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