4

I'm shading a raster layer using a QgsColorRampShader, using discrete values. Basically, I want to make the value '255' a black color because it is a null value. Here is the step-by-step through my code.

First, here is a link to the raster file I'm using: https://github.com/epurpur/PyQGIS-Scripts/blob/master/AestheticMin.tif

In this raster layer, the value range of the pixels is 3 to 900. First I calculate the value range of the raster layer, because I want to check to know what the values are:

layer = iface.activeLayer()
print("Active Layer: ", layer.name())
provider = layer.dataProvider()
extent = layer.extent()
#Using RasterBandStats to find range of values in raster layer
stats = provider.bandStatistics(1, QgsRasterBandStats.All) 
min_val = stats.minimumValue            #minimum pixel value in layer
max_val = stats.maximumValue            #maximum pixel value in layer

value_range = range(int(min_val), int(max_val+1))           #Range of values in raster layer. Without +1 doesn't capture highest value

The important part is the value_range variable, which captures the range of pixel values for the raster layer.

Because each raster will have a different value_range (probably), this is dynamic. Here is how I calculate the first half and second half values for my layer now that I know the value_range:

import numpy

first_half_max = round(numpy.percentile(value_range, 50), 2)
first_half_min = round(min_val, 2)
second_half_max = round(numpy.percentile(value_range, 100), 2)
second_half_min = round((first_half_max + .01), 2)

Now, I shade my raster layer:

raster_shader = QgsColorRampShader()
raster_shader.setColorRampType(QgsColorRampShader.Discrete)

colors_list = [ 
QgsColorRampShader.ColorRampItem(first_half_max, QColor(204, 219, 255), f"{first_half_min} - {first_half_max}"), \
QgsColorRampShader.ColorRampItem(second_half_max, QColor(153, 184, 255), f"{second_half_min} - {second_half_max}") ]

raster_shader.setColorRampItemList(colors_list)         
shader = QgsRasterShader()
shader.setRasterShaderFunction(raster_shader)       

renderer = QgsSingleBandPseudoColorRenderer(layer.dataProvider(), 1, shader)    
layer.setRenderer(renderer)
layer.triggerRepaint()

This produces the following result. This is OK, except there are pixels with a value of '255' (no value) that are lost in this map:

enter image description here

The desired output would look like this. Notice, the black values around the map and then some pixels inside the map with missing values: enter image description here

@J.Monticolo's answer below works, but only if the values of the raster layer are less than 255. If there are values greater than 255, the 255 value is lost. Here is how J.Monticolo creates the color's list (all is the same except the color's list, so I am only including colors_list):

colors_list = [ 
QgsColorRampShader.ColorRampItem(first_half_max, QColor(255, 255, 255), f"{first_half_min} - {first_half_max}"), \
QgsColorRampShader.ColorRampItem(second_half_max, QColor(153, 184, 255), f"{second_half_min} - {second_half_max}"), \
QgsColorRampShader.ColorRampItem(255, QColor(0, 0, 0), 'No Value') ]

He specifically isolates the value '255' in the last line, where he defines it as a black value. But when using this code with my raster layer, I get the following result:

enter image description here

The legend is correct. But notice, because the value 255 falls within the first_half values, it is shaded white instead of black.

Can I isolate the '255' value while using a range of values to shade the others?

5
+50

In QGIS 3.4 (Ubuntu), I've created a polygon layer (WGS 84, EPSG: 4326) with a unique "value" field, type decimal (4, 1). Here the layer datas :

wkt_geom    value
Polygon ((0.5 0.6, 0.2 0.4, 0.5 0.2, 0.8 0.4, 0.5 0.6)) 174
Polygon ((0 1, 1 0.8, 1 0, 0 0.2, 0 1),(0.5 0.6, 0.2 0.4, 0.5 0.2, 0.8 0.4, 0.5 0.6))   92.3

Or just create a diamond shape polygon with a hole filled with another polygon. The diamond shape polygon has as value 92.3 and the interior polygon 174.

I've rasterized this polygon layer : Raster Menu > Conversion > Vector to raster Here the GDAL command line (from the QGIS tool):

gdal_rasterize -l layer_name -a value -ts 300.0 300.0 -init 255.0 -a_nodata 0.0 -te 0.0 0.0 1.0 1.0 -ot Float32 -of GTiff path_to_data_file /tmp/raster.tif

For Windows users, change the path to C:\temp\raster.tif.

With the resulting raster, I applied the slightly modified script :

raster_shader = QgsColorRampShader()
raster_shader.setColorRampType(QgsColorRampShader.Discrete)

first_half_min = 91
first_half_max = 95
second_half_min = 172
second_half_max = 176

colors_list = [ 
QgsColorRampShader.ColorRampItem(first_half_max, QColor(255, 255, 255), f"{first_half_min} - {first_half_max}"), \
QgsColorRampShader.ColorRampItem(second_half_max, QColor(153, 184, 255), f"{second_half_min} - {second_half_max}"), \
QgsColorRampShader.ColorRampItem(255, QColor(0, 0, 0), 'No Value') ]

raster_shader.setColorRampItemList(colors_list)
shader = QgsRasterShader()
shader.setRasterShaderFunction(raster_shader)

layer = iface.activeLayer()
renderer = QgsSingleBandPseudoColorRenderer(layer.dataProvider(), 1, shader)

layer.setRenderer(renderer)
layer.triggerRepaint()

and it works : value 92.3 is white, value 174 is blue and 255 is black, the legend is : 91 - 95, 172 - 176 and No Value respectively.

EDIT

With the raster data, which have values beyond 255 (I generate random colors for other classes than 255) :

from random import random

raster_shader = QgsColorRampShader()
raster_shader.setColorRampType(QgsColorRampShader.Discrete)

first_half = 451.5
second_half = 900

rastvals = [first_half, second_half, 255]
rastvals.sort()

colors_list = []

i = 0
for j, k in enumerate(rastvals):
    if j == 0 and k == 255:
        col = QColor(int(random()*255), int(random()*255), int(random()*255))
        txt = f'{i} - 254.99'
        colors_list.append(QgsColorRampShader.ColorRampItem(254.99, col, txt))

        col = QColor(0, 0, 0)
        txt = 'No Value'
        colors_list.append(QgsColorRampShader.ColorRampItem(k, col, txt))
    elif k == 255:
        col = QColor(0, 0, 0)
        txt = 'No Value'
        colors_list.append(QgsColorRampShader.ColorRampItem(k, col, txt))
    else:
        col = QColor(int(random()*255), int(random()*255), int(random()*255))
        txt = f'{i} - {k}'
        colors_list.append(QgsColorRampShader.ColorRampItem(k, col, txt))

    i = k

raster_shader.setColorRampItemList(colors_list)
shader = QgsRasterShader()
shader.setRasterShaderFunction(raster_shader)

layer = iface.activeLayer()
renderer = QgsSingleBandPseudoColorRenderer(layer.dataProvider(), 1, shader)

layer.setRenderer(renderer)
layer.triggerRepaint()

EDIT BIS

A better answer with the variables of the original code. The first class begins to the minimum value, I use the original post colors (no more random !) and the class for 255 'No Value' is more clean (no more 254.99 !) :

layer = iface.activeLayer()
print("Active Layer: ", layer.name())
provider = layer.dataProvider()
extent = layer.extent()
#Using RasterBandStats to find range of values in raster layer
stats = provider.bandStatistics(1, QgsRasterBandStats.All) 
min_val = stats.minimumValue            #minimum pixel value in layer
max_val = stats.maximumValue            #maximum pixel value in layer

value_range = range(int(min_val), int(max_val+1))           #Range of values in raster layer. Without +1 doesn't capture highest value

import numpy

first_half_max = round(numpy.percentile(value_range, 50), 2)
first_half_min = round(min_val, 2)
second_half_max = round(numpy.percentile(value_range, 100), 2)
second_half_min = round((first_half_max + .01), 2)

raster_shader = QgsColorRampShader()
raster_shader.setColorRampType(QgsColorRampShader.Discrete)

rastvals = [first_half_max, second_half_max, 255]
rastvals.sort()

colors_list = []

i = first_half_min
for j, k in enumerate(rastvals):
    if j == 0 and k == 255:
        col = QColor(204, 219, 255)
        txt = f'{i} - {k}'
        colors_list.append(QgsColorRampShader.ColorRampItem(254.99, col, txt))

        col = QColor(0, 0, 0)
        txt = 'No Value'
    elif k == 255:
        col = QColor(0, 0, 0)
        txt = 'No Value'
    else:
        if k == first_half_max:
            col = QColor(204, 219, 255)
        elif k == second_half_max:
            col = QColor(153, 184, 255)
        txt = f'{i} - {k}'

    colors_list.append(QgsColorRampShader.ColorRampItem(k, col, txt))
    i = k

raster_shader.setColorRampItemList(colors_list)
shader = QgsRasterShader()
shader.setRasterShaderFunction(raster_shader)

renderer = QgsSingleBandPseudoColorRenderer(layer.dataProvider(), 1, shader)

layer.setRenderer(renderer)
layer.triggerRepaint()
  • Thank you for the response. This solution works well if the raster layer has values less than 255. However, it doesn't if the raster has values greater than 255. The 255 value is lost with the other values. I will revise my question, with a link to my real raster file and my complete code. – Erich Purpur Feb 14 at 15:23
  • 1
    Thanks for the data, I've edited my answer with a working code. – J. Monticolo Feb 14 at 16:32
  • 1
    I've improved the code in my EDIT BIS section. – J. Monticolo Feb 15 at 8:11
  • Wow, this is great. Thank you! – Erich Purpur Feb 15 at 14:42

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

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