0

I need to run some calculations on some raster points, as I'm trying to find the highest point in a given radius around where the mouse is clicked. So far I've managed to get the X and Y coordinates clicked by the mouse, and a radius value specified by the user. I've also been able to create a QgsCircle object around this point with the given radius as shown below:

pointX = clickedPoint[0]
pointY = clickedPoint[1]
point = QgsPoint(pointX, pointY)
radius = self.sbRadius.value()
mainRaster = self.cbTerrainLayer.currentLayer()
circleToChooseFrom = QgsCircle(point, radius)

Where would I go next from here, if I was wanting to get this circular portion of the main raster, with all of the point and elevation values in the radius? I was thinking of using the qgis drape (setzfromraster) processing tool, on the QgsCircle, but this seems to require a vector layer (not a QgsCircle) and I'm not entirely sure if it would do what is needed.

2
  • 1
    Do you really need all the pixel values within the radius distance? If you just need the maximum value I would suggest using QgsZonalStatistics.calculateStatistics(). I would create a QgsGeometry object from the clicked point and buffer by the radius distance instead of creating a QgsCircle. Don't forget you may need to convert the buffer distance to canvas CRS units and then transform to the raster CRS if different. Let me know if this would suit your needs and I will add a full answer tomorrow.
    – Ben W
    Commented Mar 23 at 11:29
  • 1
    @BenW Thanks, that looks like it would fit what I'm looking to do. I am mainly trying to get the highest point (maximum Z value), but assumed I'd have to filter through all the values to find this. I'll get started trying that out, but a full answer would be useful!
    – oliver
    Commented Mar 25 at 8:16

1 Answer 1

3

Here is an example using the QgsZonalStatistics class to find the highest elevation from a DEM within a given distance from the clicked coordinate. Specifically, we can use the calculateStatistics() static method which takes a QgsGeometry argument rather than a vector layer.

It's also worth bearing in mind that the clicked point will be in the canvas CRS. I assume that you are getting your user-defined radius distance in meters, so this example includes a check to see if the canvas CRS map units are not meters and uses the QgsDistanceArea class to convert to the appropriate unit if necessary. This answer also shows an example of using the QgsCoordinateTransform class to transform the buffer geometry to match that of the raster layer if those are different.

To also extract the locations of the pixels with the maximum value (which of course there may be more than one), we can read the raster into a numpy array and use the numpy.where() method to get the indices of all pixels with the maximum value, then calculate the coordinates of the pixel centroids based on the raster origin & pixel dimensions (georeferenced rasters do not inherently store the location of every pixel). To make this process a bit more efficient, this example makes a processing.run() call to "gdal:cliprasterbyextent" to clip the raster to the bounding box of the buffered click point, before running the zonal stats calculation.

from osgeo import gdal
import numpy as np

class CustomMapTool(QgsMapToolEmitPoint):
    
    def __init__(self, iface):
        self.iface = iface
        self.canvas = self.iface.mapCanvas()
        super(CustomMapTool, self).__init__(self.canvas)
        self.buffer_distance = 5000.0# Hard coded for this example (units are meters)
        self.da = QgsDistanceArea()
        
    def canvasReleaseEvent(self, e):
        clicked_point = e.mapPoint()
        lyr = self.iface.activeLayer()
        if lyr.type() != QgsMapLayerType.RasterLayer:
            return
        max_val, coords = self.get_zonal_max(lyr, clicked_point)
        print(max_val, coords)

    def get_zonal_max(self, rl, click_pt):
        canvas_crs = self.canvas.mapSettings().destinationCrs()
        if canvas_crs.mapUnits() != QgsUnitTypes.DistanceMeters:
            self.da.setSourceCrs(canvas_crs, self.canvas.mapSettings().transformContext())
            self.da.setEllipsoid(canvas_crs.ellipsoidAcronym())
            radius = self.da.convertLengthMeasurement(self.buffer_distance, canvas_crs.mapUnits())
        else:
            radius = self.buffer_distance
        pt_geom = QgsGeometry.fromPointXY(click_pt)
        buffer = pt_geom.buffer(radius, 50)
        
        if rl.crs() != canvas_crs:
            buffer = self.transformed_geom(buffer, canvas_crs, rl.crs())
        
        bb = buffer.boundingBox()
        ext = f'{bb.xMinimum()},{bb.xMaximum()},{bb.yMinimum()},{bb.yMaximum()}'
        params = {'INPUT':rl,
                'PROJWIN':ext,
                'OVERCRS':False,
                'NODATA':-999,
                'OPTIONS':'',
                'DATA_TYPE':0,
                'EXTRA':'',
                'OUTPUT':'TEMPORARY_OUTPUT'}
                
        temp_rl = QgsRasterLayer(processing.run("gdal:cliprasterbyextent", params)['OUTPUT'], '', 'gdal')
        
        pixel_size_X = temp_rl.rasterUnitsPerPixelX()
        pixel_size_Y = temp_rl.rasterUnitsPerPixelY()
        res = QgsZonalStatistics.calculateStatistics(temp_rl.dataProvider(),
                                                    buffer,
                                                    pixel_size_X,
                                                    pixel_size_Y,
                                                    1,
                                                    QgsZonalStatistics.Statistic.Max)
        if not list(res):
            return NULL, []
        max_val = res[list(res)[0]]
        max_coords = []
        highest_pts = self.get_pixel_coords_from_val(temp_rl, max_val)
        for pt in highest_pts:
            pt_geom = QgsGeometry.fromPointXY(pt)
            if pt_geom.within(buffer):
                max_coords.append((pt.x(), pt.y()))
        return max_val, max_coords
        
    def transformed_geom(self, g, src_crs, dest_crs):
        xform = QgsCoordinateTransform(src_crs, dest_crs, QgsProject.instance())
        g.transform(xform)
        return g
        
    def get_pixel_coords_from_val(self, r_lyr, val):
        path = r_lyr.source()
        ds = gdal.Open(path)
        geotransform = ds.GetGeoTransform()
        originX = geotransform[0]
        originY = geotransform[3]
        pixelWidth = geotransform[1]
        pixelHeight = geotransform[5]
        arr = ds.ReadAsArray()
        value_indices = list(zip(*np.where(arr == val)))
        all_points = []
        for pp in value_indices:
            x = (originX+(pp[1]*pixelWidth))+pixelWidth/2
            y = (originY+(pp[0]*pixelHeight))+pixelHeight/2# Pixel height is negative
            all_points.append(QgsPointXY(x, y))
        ds = None
        return all_points
        
map_tool = CustomMapTool(iface)
iface.mapCanvas().setMapTool(map_tool)

This script will print the maximum value plus a list containing tuples of coordinate pairs for each of the pixels having the maximum elevation within the buffer distance of the click point, which you can then do with whatever you like. Just as an example, in the screencast below, I added some logic to the script to create features and add them to a temporary layer.

enter image description here

5
  • Thanks for the answer. I've managed to implement that and it's working properly. I was wondering if it was possible to get the coordinates (X, Y) of this point with highest elevation? As right now it's just the Z value.
    – oliver
    Commented Mar 25 at 11:32
  • @oliver, it will be possible but a bit more involved. I will try to update my answer tomorrow (it's getting late now).
    – Ben W
    Commented Mar 25 at 12:20
  • no worries, thanks for the help so far!
    – oliver
    Commented Mar 25 at 15:14
  • 1
    @oliver, I have updated my answer/script to return the locations of the pixels with the maximum elevation, which should do what you are after.
    – Ben W
    Commented Mar 27 at 7:10
  • 1
    thank you very much, this did exactly what I needed!
    – oliver
    Commented Mar 27 at 15:24

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.