Skip to content

Instantly share code, notes, and snippets.

@bennyistanto
Last active August 27, 2024 13:15
Show Gist options
  • Save bennyistanto/7ec367ba090468ca0e6e888dd13f7f3e to your computer and use it in GitHub Desktop.
Save bennyistanto/7ec367ba090468ca0e6e888dd13f7f3e to your computer and use it in GitHub Desktop.
Test NTL data process for GEEST

PyQGIS NTL Clipping, Classification, and Visualization

Screenshot 2024-08-27 194434

Overview

This script is designed to run within the PyQGIS console and provides a complete workflow for processing Night Time Lights (NTL) data. The script performs the following tasks:

  1. Clipping - Clips the global NTL raster data to the boundary of a specified country or region.
  2. Classification - Classifies the clipped NTL data into 6 classes using the GEEST Safety Classification standard.
  3. Visualization - Applies a predefined QML style to the classified NTL data for visualization in QGIS.

Features

  • Dynamic NODATA Handling: Automatically determines and applies an appropriate NODATA value based on the input raster's data type.
  • Dual Classification Schemes: Precomputes thresholds for both the original and max_value classification schemes, allowing for flexible and consistent processing.
  • Automated CRS Reprojection: Ensures that the boundary shapefile and NTL raster data are in the same coordinate reference system.
  • Integrated QML Styling: Applies a QML style to the classified NTL raster for consistent visualization in QGIS.

Requirements

  • QGIS with Python support (PyQGIS)

Usage

  1. Copy and paste the script into the PyQGIS console.
  2. Adjust the file paths for the NTL raster, boundary shapefile, and output files as needed.
  3. Run the script to perform the NTL clipping, classification, and visualization tasks.

Script Workflow

  1. Load Input Layers: The script first loads the global NTL raster and the boundary shapefile into QGIS.
  2. Clip NTL Data: The NTL raster is clipped to the specified boundary, with dynamic handling of NODATA values based on the raster's data type.
  3. Classify NTL Data: The clipped NTL data is classified into 6 classes according to the GEEST standard. Both the original and max_value classification schemes are precomputed, allowing the user to choose the most appropriate scheme.
  4. Apply Visualization Style: The classified NTL raster is styled using a predefined QML file to provide a clear visual representation of the classification.
import os
import numpy as np
from qgis.core import (QgsRasterLayer, QgsVectorLayer, QgsProject)
import processing
def add_layer_if_not_exists(layer, layer_name, zoom_to_layer=False):
"""Add layer to QGIS project if it doesn't already exist, optionally zoom to it."""
if not QgsProject.instance().mapLayersByName(layer_name):
QgsProject.instance().addMapLayer(layer)
print(f"Layer {layer_name} added to QGIS project.")
if zoom_to_layer:
iface.mapCanvas().setExtent(layer.extent())
iface.mapCanvas().refresh()
print(f"Zoomed to the {layer_name} layer.")
else:
print(f"Layer {layer_name} already exists in QGIS project.")
def set_boundary_layer_style(layer):
"""Set the boundary layer to have transparent fill and black outline."""
symbol = QgsFillSymbol.createSimple({'color': 'transparent', 'outline_color': 'black', 'outline_width': '0.6'})
layer.renderer().setSymbol(symbol)
layer.triggerRepaint()
print("Boundary layer style updated: transparent fill, black outline.")
def determine_nodata_value_and_type(data_type):
"""Determine a suitable NODATA value and the corresponding GDAL data type based on the raster data type."""
if data_type == 1: # Byte
return 255, 1
elif data_type == 2: # UInt16
return 65535, 3
elif data_type == 3: # Int16
return -32768, 2
elif data_type == 4: # UInt32
return 4294967295, 4
elif data_type == 5: # Int32
return -2147483648, 5
elif data_type == 6: # Float32
return -3.40282346639e+38, 6
elif data_type == 7: # Float64
return -1.7976931348623157e+308, 7
elif data_type == 12: # Int8
return -128, 12
else:
raise ValueError(f"Unsupported data type: {data_type}")
def clip_ntl(input_global_tif, country_boundary_shp, output_clipped_tif):
print("Loading raster and vector layers...")
ntl_layer = QgsRasterLayer(input_global_tif, "NTL")
boundary_layer = QgsVectorLayer(country_boundary_shp, "Boundary", "ogr")
if not ntl_layer.isValid():
print("Error: NTL layer failed to load.")
return
if not boundary_layer.isValid():
print("Error: Boundary layer failed to load.")
return
add_layer_if_not_exists(ntl_layer, "NTL")
add_layer_if_not_exists(boundary_layer, "Boundary", zoom_to_layer=True)
# Set boundary layer style to transparent fill with black outline
set_boundary_layer_style(boundary_layer)
# Ensure CRS match
print("Checking CRS of boundary and NTL layers...")
if boundary_layer.crs() != ntl_layer.crs():
print("Reprojecting boundary layer to match NTL CRS...")
params = {
'INPUT': boundary_layer,
'TARGET_CRS': ntl_layer.crs(),
'OUTPUT': 'memory:'
}
boundary_layer = processing.run("native:reprojectlayer", params)['OUTPUT']
if not boundary_layer.isValid():
print("Error: Failed to reproject boundary layer.")
return
# Determine NODATA value and GDAL data type based on the data type of the input raster
data_type = ntl_layer.dataProvider().dataType(1) # Get data type of the first band
nodata_value, gdal_data_type = determine_nodata_value_and_type(data_type)
# Use gdal:cliprasterbymasklayer with the dynamically determined nodata value and additional config
print(f"Clipping the NTL raster with gdal:cliprasterbymasklayer using NODATA={nodata_value} and DATA_TYPE={gdal_data_type}...")
params = {
'INPUT': ntl_layer,
'MASK': boundary_layer,
'NODATA': nodata_value,
'ALPHA_BAND': False,
'CROP_TO_CUTLINE': True,
'KEEP_RESOLUTION': True,
'SET_RESOLUTION': False,
'X_RESOLUTION': None,
'Y_RESOLUTION': None,
'MULTITHREADING': False,
'OPTIONS': f'--config GDALWARP_IGNORE_BAD_CUTLINE=YES -dstnodata {nodata_value}',
'DATA_TYPE': gdal_data_type,
'OUTPUT': output_clipped_tif
}
result = processing.run("gdal:cliprasterbymasklayer", params)
# Check if the output file was created and loaded correctly
if os.path.exists(output_clipped_tif):
print(f"Clipped NTL raster saved to {output_clipped_tif}")
clipped_layer = QgsRasterLayer(output_clipped_tif, "Clipped NTL")
if not clipped_layer.isValid():
print("Error: Failed to load the clipped NTL raster into QGIS.")
return
QgsProject.instance().addMapLayer(clipped_layer)
print("Clipped NTL raster added to QGIS project.")
else:
print(f"Error: Clipping failed. The output file {output_clipped_tif} was not created.")
return
def apply_qml_style(layer, qml_path):
"""Apply QML style to a layer."""
if os.path.exists(qml_path):
layer.loadNamedStyle(qml_path)
layer.triggerRepaint()
print(f"QML style applied from {qml_path}")
else:
print(f"Error: QML file {qml_path} not found.")
def classify_ntl(input_clipped_tif, output_classified_tif, qml_path, use_max_value_scheme=False):
print("Loading clipped NTL raster for classification...")
layer = QgsRasterLayer(input_clipped_tif, "Clipped NTL")
if not layer.isValid():
print("Error: Clipped NTL raster failed to load.")
return
provider = layer.dataProvider()
extent = layer.extent()
cols = layer.width()
rows = layer.height()
block = provider.block(1, extent, cols, rows)
print("Converting raster block to numpy array...")
nodata = provider.sourceNoDataValue(1)
data = np.array([[block.value(i, j) for j in range(cols)] for i in range(rows)])
# Ensure consistency in handling nodata
valid_data = data[~np.isnan(data) & (data != nodata)]
if valid_data.size > 0:
# Calculate statistics
min_value = np.min(valid_data)
max_value = np.max(valid_data)
median = np.median(valid_data)
percentile_75 = np.percentile(valid_data, 75)
print(f"Min Value: {min_value:.6f}")
print(f"Max Value: {max_value:.6f}")
print(f"Median: {median:.6f}")
print(f"75th Percentile: {percentile_75:.6f}")
else:
print("No valid data found.")
# Precompute thresholds for both schemes
threshold_1 = 0.05
threshold_2 = 0.25 * median
threshold_3 = 0.5 * median
threshold_4 = median
threshold_5 = percentile_75
threshold_6 = max_value
max_value_thresholds = {
"threshold_1": 0.05,
"threshold_2": 0.2 * max_value,
"threshold_3": 0.4 * max_value,
"threshold_4": 0.6 * max_value,
"threshold_5": 0.8 * max_value,
"threshold_6": max_value,
}
print("Precomputed thresholds for original scheme:")
print(f"Threshold 1: {threshold_1}")
print(f"Threshold 2: {threshold_2}")
print(f"Threshold 3: {threshold_3}")
print(f"Threshold 4: {threshold_4}")
print(f"Threshold 5: {threshold_5}")
print(f"Threshold 6: {threshold_6}")
print("Precomputed thresholds for max_value scheme:")
for key, value in max_value_thresholds.items():
print(f"{key}: {value:.6f}")
if use_max_value_scheme or max_value <= 0.05 or (max_value - percentile_75) <= 0.05 * max_value:
print("Using max_value classification scheme")
expr_str = (f'("Clipped NTL@1" <= {max_value_thresholds["threshold_1"]}) * 0 + '
f'("Clipped NTL@1" > {max_value_thresholds["threshold_1"]} AND "Clipped NTL@1" <= {max_value_thresholds["threshold_2"]}) * 1 + '
f'("Clipped NTL@1" > {max_value_thresholds["threshold_2"]} AND "Clipped NTL@1" <= {max_value_thresholds["threshold_3"]}) * 2 + '
f'("Clipped NTL@1" > {max_value_thresholds["threshold_3"]} AND "Clipped NTL@1" <= {max_value_thresholds["threshold_4"]}) * 3 + '
f'("Clipped NTL@1" > {max_value_thresholds["threshold_4"]} AND "Clipped NTL@1" <= {max_value_thresholds["threshold_5"]}) * 4 + '
f'("Clipped NTL@1" > {max_value_thresholds["threshold_5"]}) * 5')
else:
print("Using original classification scheme")
expr_str = (f'("Clipped NTL@1" <= {threshold_1}) * 0 + '
f'("Clipped NTL@1" > {threshold_1} AND "Clipped NTL@1" <= {threshold_2}) * 1 + '
f'("Clipped NTL@1" > {threshold_2} AND "Clipped NTL@1" <= {threshold_3}) * 2 + '
f'("Clipped NTL@1" > {threshold_3} AND "Clipped NTL@1" <= {threshold_4}) * 3 + '
f'("Clipped NTL@1" > {threshold_4} AND "Clipped NTL@1" <= {threshold_5}) * 4 + '
f'("Clipped NTL@1" > {threshold_5}) * 5')
print(f"Final classification expression: {expr_str}")
entries = []
ras = QgsRasterCalculatorEntry()
ras.ref = 'Clipped NTL@1'
ras.raster = layer
ras.bandNumber = 1
entries.append(ras)
print("Raster calculator entries:")
for entry in entries:
print(entry.ref, entry.raster, entry.bandNumber)
calc = QgsRasterCalculator(expr_str, output_classified_tif, 'GTiff',
extent, cols, rows, entries)
result = calc.processCalculation()
if result == 0:
print(f"Classified NTL raster saved to {output_classified_tif}")
classified_layer = QgsRasterLayer(output_classified_tif, "Classified NTL")
if not classified_layer.isValid():
print("Error: Failed to load the classified NTL raster into QGIS.")
return
QgsProject.instance().addMapLayer(classified_layer)
print("Classified NTL raster added to QGIS project.")
# Apply QML style
apply_qml_style(classified_layer, qml_path)
else:
print(f"Error occurred during classification. Error code: {result}")
print("Starting process...")
dir = r'D:\temp\geest\factor_safety'
input_global_tif = os.path.join(dir, 'ntl', 'VNL_npp_2023_global_vcmslcfg_v2_c202402081600.average.dat.tif')
country_boundary_shp = os.path.join(dir, 'bnd', 'lca_admbnda_adm0_gov_2019.shp')
output_clipped_tif = os.path.join(dir, 'ntl', 'lca_VNL_npp_2023_global_vcmslcfg_v2_c202402081600.average.dat.tif')
output_classified_tif = os.path.join(dir, 'ntl', 'lca_ntl_geest_class.tif')
qml_path = r'D:\temp\geest\factor_safety\symbology\geest_ntl.qml'
clip_ntl(input_global_tif, country_boundary_shp, output_clipped_tif)
classify_ntl(output_clipped_tif, output_classified_tif, qml_path, use_max_value_scheme=False)
print("Process completed.")
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
<qgis styleCategories="AllStyleCategories" minScale="1e+08" version="3.34.4-Prizren" hasScaleBasedVisibilityFlag="0" maxScale="0">
<flags>
<Identifiable>1</Identifiable>
<Removable>1</Removable>
<Searchable>1</Searchable>
<Private>0</Private>
</flags>
<temporal enabled="0" mode="0" fetchMode="0">
<fixedRange>
<start></start>
<end></end>
</fixedRange>
</temporal>
<elevation enabled="0" zoffset="0" band="1" symbology="Line" zscale="1">
<data-defined-properties>
<Option type="Map">
<Option type="QString" value="" name="name"/>
<Option name="properties"/>
<Option type="QString" value="collection" name="type"/>
</Option>
</data-defined-properties>
<profileLineSymbol>
<symbol type="line" force_rhr="0" frame_rate="10" alpha="1" is_animated="0" clip_to_extent="1" name="">
<data_defined_properties>
<Option type="Map">
<Option type="QString" value="" name="name"/>
<Option name="properties"/>
<Option type="QString" value="collection" name="type"/>
</Option>
</data_defined_properties>
<layer enabled="1" pass="0" class="SimpleLine" locked="0" id="{600d5e3d-c500-42d2-ab38-887abfb1175c}">
<Option type="Map">
<Option type="QString" value="0" name="align_dash_pattern"/>
<Option type="QString" value="square" name="capstyle"/>
<Option type="QString" value="5;2" name="customdash"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="customdash_map_unit_scale"/>
<Option type="QString" value="MM" name="customdash_unit"/>
<Option type="QString" value="0" name="dash_pattern_offset"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="dash_pattern_offset_map_unit_scale"/>
<Option type="QString" value="MM" name="dash_pattern_offset_unit"/>
<Option type="QString" value="0" name="draw_inside_polygon"/>
<Option type="QString" value="bevel" name="joinstyle"/>
<Option type="QString" value="225,89,137,255" name="line_color"/>
<Option type="QString" value="solid" name="line_style"/>
<Option type="QString" value="0.6" name="line_width"/>
<Option type="QString" value="MM" name="line_width_unit"/>
<Option type="QString" value="0" name="offset"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="offset_map_unit_scale"/>
<Option type="QString" value="MM" name="offset_unit"/>
<Option type="QString" value="0" name="ring_filter"/>
<Option type="QString" value="0" name="trim_distance_end"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="trim_distance_end_map_unit_scale"/>
<Option type="QString" value="MM" name="trim_distance_end_unit"/>
<Option type="QString" value="0" name="trim_distance_start"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="trim_distance_start_map_unit_scale"/>
<Option type="QString" value="MM" name="trim_distance_start_unit"/>
<Option type="QString" value="0" name="tweak_dash_pattern_on_corners"/>
<Option type="QString" value="0" name="use_custom_dash"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="width_map_unit_scale"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option type="QString" value="" name="name"/>
<Option name="properties"/>
<Option type="QString" value="collection" name="type"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
</profileLineSymbol>
<profileFillSymbol>
<symbol type="fill" force_rhr="0" frame_rate="10" alpha="1" is_animated="0" clip_to_extent="1" name="">
<data_defined_properties>
<Option type="Map">
<Option type="QString" value="" name="name"/>
<Option name="properties"/>
<Option type="QString" value="collection" name="type"/>
</Option>
</data_defined_properties>
<layer enabled="1" pass="0" class="SimpleFill" locked="0" id="{de49674b-4a58-49b5-a8dd-64c826788e9f}">
<Option type="Map">
<Option type="QString" value="3x:0,0,0,0,0,0" name="border_width_map_unit_scale"/>
<Option type="QString" value="225,89,137,255" name="color"/>
<Option type="QString" value="bevel" name="joinstyle"/>
<Option type="QString" value="0,0" name="offset"/>
<Option type="QString" value="3x:0,0,0,0,0,0" name="offset_map_unit_scale"/>
<Option type="QString" value="MM" name="offset_unit"/>
<Option type="QString" value="35,35,35,255" name="outline_color"/>
<Option type="QString" value="no" name="outline_style"/>
<Option type="QString" value="0.26" name="outline_width"/>
<Option type="QString" value="MM" name="outline_width_unit"/>
<Option type="QString" value="solid" name="style"/>
</Option>
<data_defined_properties>
<Option type="Map">
<Option type="QString" value="" name="name"/>
<Option name="properties"/>
<Option type="QString" value="collection" name="type"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
</profileFillSymbol>
</elevation>
<customproperties>
<Option type="Map">
<Option type="bool" value="false" name="WMSBackgroundLayer"/>
<Option type="bool" value="false" name="WMSPublishDataSourceUrl"/>
<Option type="int" value="0" name="embeddedWidgets/count"/>
<Option type="QString" value="Value" name="identify/format"/>
</Option>
</customproperties>
<mapTip enabled="1"></mapTip>
<pipe-data-defined-properties>
<Option type="Map">
<Option type="QString" value="" name="name"/>
<Option name="properties"/>
<Option type="QString" value="collection" name="type"/>
</Option>
</pipe-data-defined-properties>
<pipe>
<provider>
<resampling enabled="false" maxOversampling="2" zoomedInResamplingMethod="nearestNeighbour" zoomedOutResamplingMethod="nearestNeighbour"/>
</provider>
<rasterrenderer type="singlebandpseudocolor" classificationMin="0" opacity="1" nodataColor="" band="1" alphaBand="-1" classificationMax="5">
<rasterTransparency/>
<minMaxOrigin>
<limits>None</limits>
<extent>WholeRaster</extent>
<statAccuracy>Estimated</statAccuracy>
<cumulativeCutLower>0.02</cumulativeCutLower>
<cumulativeCutUpper>0.98</cumulativeCutUpper>
<stdDevFactor>2</stdDevFactor>
</minMaxOrigin>
<rastershader>
<colorrampshader classificationMode="2" clip="0" colorRampType="INTERPOLATED" maximumValue="5" labelPrecision="4" minimumValue="0">
<colorramp type="gradient" name="[source]">
<Option type="Map">
<Option type="QString" value="103,0,13,255" name="color1"/>
<Option type="QString" value="255,245,240,255" name="color2"/>
<Option type="QString" value="cw" name="direction"/>
<Option type="QString" value="0" name="discrete"/>
<Option type="QString" value="gradient" name="rampType"/>
<Option type="QString" value="rgb" name="spec"/>
<Option type="QString" value="0.1;165,15,21,255;rgb;cw:0.22;203,24,29,255;rgb;cw:0.35;239,59,44,255;rgb;cw:0.48;251,106,74,255;rgb;cw:0.61;252,146,114,255;rgb;cw:0.74;252,187,161,255;rgb;cw:0.87;254,224,210,255;rgb;cw" name="stops"/>
</Option>
</colorramp>
<item value="0" alpha="255" label="0.0000" color="#67000d"/>
<item value="1" alpha="255" label="1.0000" color="#c5171c"/>
<item value="2" alpha="255" label="2.0000" color="#f44d38"/>
<item value="3" alpha="255" label="3.0000" color="#fc8f6f"/>
<item value="4" alpha="255" label="4.0000" color="#fdccb8"/>
<item value="5" alpha="255" label="5.0000" color="#fff5f0"/>
<rampLegendSettings maximumLabel="" suffix="" direction="0" orientation="2" useContinuousLegend="1" minimumLabel="" prefix="">
<numericFormat id="basic">
<Option type="Map">
<Option type="invalid" name="decimal_separator"/>
<Option type="int" value="6" name="decimals"/>
<Option type="int" value="0" name="rounding_type"/>
<Option type="bool" value="false" name="show_plus"/>
<Option type="bool" value="true" name="show_thousand_separator"/>
<Option type="bool" value="false" name="show_trailing_zeros"/>
<Option type="invalid" name="thousand_separator"/>
</Option>
</numericFormat>
</rampLegendSettings>
</colorrampshader>
</rastershader>
</rasterrenderer>
<brightnesscontrast brightness="0" contrast="0" gamma="1"/>
<huesaturation invertColors="0" colorizeOn="0" colorizeStrength="100" grayscaleMode="0" saturation="0" colorizeGreen="128" colorizeBlue="128" colorizeRed="255"/>
<rasterresampler maxOversampling="2"/>
<resamplingStage>resamplingFilter</resamplingStage>
</pipe>
<blendMode>0</blendMode>
</qgis>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment