QGIS API Documentation 3.43.0-Master (58029bba303)
qgsalgorithmrasterdtmslopebasedfilter.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsalgorithmrasterdtmslopebasedfilter.cpp
3 ---------------------
4 begin : July 2023
5 copyright : (C) 2023 by Nyall Dawson
6 email : nyall dot dawson at gmail dot com
7 ***************************************************************************/
8
9/***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
19#include "qgsrasterfilewriter.h"
20#include <algorithm>
21
23
24QString QgsRasterDtmSlopeBasedFilterAlgorithm::name() const
25{
26 return QStringLiteral( "dtmslopebasedfilter" );
27}
28
29QString QgsRasterDtmSlopeBasedFilterAlgorithm::displayName() const
30{
31 return QObject::tr( "DTM filter (slope-based)" );
32}
33
34QStringList QgsRasterDtmSlopeBasedFilterAlgorithm::tags() const
35{
36 return QObject::tr( "dem,filter,slope,dsm,dtm,terrain" ).split( ',' );
37}
38
39QString QgsRasterDtmSlopeBasedFilterAlgorithm::group() const
40{
41 return QObject::tr( "Raster terrain analysis" );
42}
43
44QString QgsRasterDtmSlopeBasedFilterAlgorithm::groupId() const
45{
46 return QStringLiteral( "rasterterrainanalysis" );
47}
48
49QString QgsRasterDtmSlopeBasedFilterAlgorithm::shortHelpString() const
50{
51 return QObject::tr( "This algorithm can be used to filter a digital elevation model in order to classify its cells into ground and object (non-ground) cells.\n\n"
52 "The tool uses concepts as described by Vosselman (2000) and is based on the assumption that a large height difference between two nearby "
53 "cells is unlikely to be caused by a steep slope in the terrain. The probability that the higher cell might be non-ground increases when "
54 "the distance between the two cells decreases. Therefore the filter defines a maximum height difference (<i>dz_max</i>) between two cells as a "
55 "function of the distance (<i>d</i>) between the cells (<i>dz_max( d ) = d</i>).\n\n"
56 "A cell is classified as terrain if there is no cell within the kernel radius to which the height difference is larger than the allowed "
57 "maximum height difference at the distance between these two cells.\n\n"
58 "The approximate terrain slope (<i>s</i>) parameter is used to modify the filter function to match the overall slope in the study "
59 "area (<i>dz_max( d ) = d * s</i>).\n\n"
60 "A 5 % confidence interval (<i>ci = 1.65 * sqrt( 2 * stddev )</i>) may be used to modify the filter function even further by either "
61 "relaxing (<i>dz_max( d ) = d * s + ci</i>) or amplifying (<i>dz_max( d ) = d * s - ci</i>) the filter criterium.\n\n"
62 "References: Vosselman, G. (2000): Slope based filtering of laser altimetry data. IAPRS, Vol. XXXIII, Part B3, Amsterdam, The Netherlands, 935-942\n\n"
63 "This algorithm is a port of the SAGA 'DTM Filter (slope-based)' tool." );
64}
65
66void QgsRasterDtmSlopeBasedFilterAlgorithm::initAlgorithm( const QVariantMap & )
67{
68 addParameter( new QgsProcessingParameterRasterLayer( QStringLiteral( "INPUT" ), QObject::tr( "Input layer" ) ) );
69
70 addParameter( new QgsProcessingParameterBand( QStringLiteral( "BAND" ), QObject::tr( "Band number" ), 1, QStringLiteral( "INPUT" ) ) );
71
72 auto radiusParam = std::make_unique<QgsProcessingParameterNumber>( QStringLiteral( "RADIUS" ), QObject::tr( "Kernel radius (pixels)" ), Qgis::ProcessingNumberParameterType::Integer, 5, false, 1, 1000 );
73 radiusParam->setHelp( QObject::tr( "The radius of the filter kernel (in pixels). Must be large enough to reach ground cells next to non-ground objects." ) );
74 addParameter( radiusParam.release() );
75
76 auto terrainSlopeParam = std::make_unique<QgsProcessingParameterNumber>( QStringLiteral( "TERRAIN_SLOPE" ), QObject::tr( "Terrain slope (%, pixel size/vertical units)" ), Qgis::ProcessingNumberParameterType::Double, 30, false, 0, 1000 );
77 terrainSlopeParam->setHelp( QObject::tr( "The approximate terrain slope in %. The terrain slope must be adjusted to account for the ratio of height units vs raster pixel dimensions. Used to relax the filter criterium in steeper terrain." ) );
78 addParameter( terrainSlopeParam.release() );
79
80 auto filterModificationParam = std::make_unique<QgsProcessingParameterEnum>( QStringLiteral( "FILTER_MODIFICATION" ), QObject::tr( "Filter modification" ), QStringList { QObject::tr( "None" ), QObject::tr( "Relax filter" ), QObject::tr( "Amplify" ) }, false, 0 );
81 filterModificationParam->setHelp( QObject::tr( "Choose whether to apply the filter kernel without modification or to use a confidence interval to relax or amplify the height criterium." ) );
82 addParameter( filterModificationParam.release() );
83
84 auto stDevParam = std::make_unique<QgsProcessingParameterNumber>( QStringLiteral( "STANDARD_DEVIATION" ), QObject::tr( "Standard deviation" ), Qgis::ProcessingNumberParameterType::Double, 0.1, false, 0, 1000 );
85 stDevParam->setHelp( QObject::tr( "The standard deviation used to calculate a 5% confidence interval applied to the height threshold." ) );
86 addParameter( stDevParam.release() );
87
88 // backwards compatibility parameter
89 // TODO QGIS 4: remove parameter and related logic
90 auto createOptsParam = std::make_unique<QgsProcessingParameterString>( QStringLiteral( "CREATE_OPTIONS" ), QObject::tr( "Creation options" ), QVariant(), false, true );
91 createOptsParam->setMetadata( QVariantMap( { { QStringLiteral( "widget_wrapper" ), QVariantMap( { { QStringLiteral( "widget_type" ), QStringLiteral( "rasteroptions" ) } } ) } } ) );
92 createOptsParam->setFlags( createOptsParam->flags() | Qgis::ProcessingParameterFlag::Hidden );
93 addParameter( createOptsParam.release() );
94
95 auto creationOptsParam = std::make_unique<QgsProcessingParameterString>( QStringLiteral( "CREATION_OPTIONS" ), QObject::tr( "Creation options" ), QVariant(), false, true );
96 creationOptsParam->setMetadata( QVariantMap( { { QStringLiteral( "widget_wrapper" ), QVariantMap( { { QStringLiteral( "widget_type" ), QStringLiteral( "rasteroptions" ) } } ) } } ) );
97 creationOptsParam->setFlags( creationOptsParam->flags() | Qgis::ProcessingParameterFlag::Advanced );
98 addParameter( creationOptsParam.release() );
99
100 auto outputLayerGroundParam = std::make_unique<QgsProcessingParameterRasterDestination>( QStringLiteral( "OUTPUT_GROUND" ), QObject::tr( "Output layer (ground)" ), QVariant(), true, true );
101 outputLayerGroundParam->setHelp( QObject::tr( "The filtered DEM containing only cells classified as ground." ) );
102 addParameter( outputLayerGroundParam.release() );
103
104 auto outputLayerNonGroundParam = std::make_unique<QgsProcessingParameterRasterDestination>( QStringLiteral( "OUTPUT_NONGROUND" ), QObject::tr( "Output layer (non-ground objects)" ), QVariant(), true, false );
105 outputLayerNonGroundParam->setHelp( QObject::tr( "The non-ground objects removed by the filter." ) );
106 addParameter( outputLayerNonGroundParam.release() );
107}
108
109QgsRasterDtmSlopeBasedFilterAlgorithm *QgsRasterDtmSlopeBasedFilterAlgorithm::createInstance() const
110{
111 return new QgsRasterDtmSlopeBasedFilterAlgorithm();
112}
113
114bool QgsRasterDtmSlopeBasedFilterAlgorithm::prepareAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback * )
115{
116 QgsRasterLayer *layer = parameterAsRasterLayer( parameters, QStringLiteral( "INPUT" ), context );
117 if ( !layer )
118 throw QgsProcessingException( invalidRasterError( parameters, QStringLiteral( "INPUT" ) ) );
119
120 const int band = parameterAsInt( parameters, QStringLiteral( "BAND" ), context );
121
122 mBand = parameterAsInt( parameters, QStringLiteral( "BAND" ), context );
123 if ( mBand < 1 || mBand > layer->bandCount() )
124 throw QgsProcessingException( QObject::tr( "Invalid band number for BAND (%1): Valid values for input raster are 1 to %2" ).arg( mBand ).arg( layer->bandCount() ) );
125
126 mInterface.reset( layer->dataProvider()->clone() );
127 mHasNoDataValue = layer->dataProvider()->sourceHasNoDataValue( band );
128 mLayerWidth = layer->width();
129 mLayerHeight = layer->height();
130 mExtent = layer->extent();
131 mCrs = layer->crs();
132 mRasterUnitsPerPixelX = layer->rasterUnitsPerPixelX();
133 mRasterUnitsPerPixelY = layer->rasterUnitsPerPixelY();
134 mDataType = layer->dataProvider()->dataType( mBand );
135 mNoData = layer->dataProvider()->sourceNoDataValue( mBand );
136 return true;
137}
138
139QVariantMap QgsRasterDtmSlopeBasedFilterAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
140{
141 QString creationOptions = parameterAsString( parameters, QStringLiteral( "CREATION_OPTIONS" ), context ).trimmed();
142 // handle backwards compatibility parameter CREATE_OPTIONS
143 const QString optionsString = parameterAsString( parameters, QStringLiteral( "CREATE_OPTIONS" ), context );
144 if ( !optionsString.isEmpty() )
145 creationOptions = optionsString;
146
147 const QString groundOutputFile = parameterAsOutputLayer( parameters, QStringLiteral( "OUTPUT_GROUND" ), context );
148 std::unique_ptr<QgsRasterFileWriter> groundWriter;
149 std::unique_ptr<QgsRasterDataProvider> groundDestProvider;
150
151 if ( !groundOutputFile.isEmpty() )
152 {
153 const QFileInfo fi( groundOutputFile );
154 const QString outputFormat = QgsRasterFileWriter::driverForExtension( fi.suffix() );
155
156 groundWriter = std::make_unique<QgsRasterFileWriter>( groundOutputFile );
157 groundWriter->setOutputProviderKey( QStringLiteral( "gdal" ) );
158 if ( !creationOptions.isEmpty() )
159 {
160 groundWriter->setCreationOptions( creationOptions.split( '|' ) );
161 }
162 groundWriter->setOutputFormat( outputFormat );
163
164 groundDestProvider.reset( groundWriter->createOneBandRaster( mDataType, mLayerWidth, mLayerHeight, mExtent, mCrs ) );
165
166 if ( !groundDestProvider )
167 throw QgsProcessingException( QObject::tr( "Could not create raster output: %1" ).arg( groundOutputFile ) );
168 if ( !groundDestProvider->isValid() )
169 throw QgsProcessingException( QObject::tr( "Could not create raster output %1: %2" ).arg( groundOutputFile, groundDestProvider->error().message( QgsErrorMessage::Text ) ) );
170
171 groundDestProvider->setNoDataValue( 1, mNoData );
172 groundDestProvider->setEditable( true );
173 }
174
175 const QString nonGroundOutputFile = parameterAsOutputLayer( parameters, QStringLiteral( "OUTPUT_NONGROUND" ), context );
176 std::unique_ptr<QgsRasterFileWriter> nonGroundWriter;
177 std::unique_ptr<QgsRasterDataProvider> nonGroundDestProvider;
178
179 if ( !nonGroundOutputFile.isEmpty() )
180 {
181 const QFileInfo fi( groundOutputFile );
182 const QString outputFormat = QgsRasterFileWriter::driverForExtension( fi.suffix() );
183
184 nonGroundWriter = std::make_unique<QgsRasterFileWriter>( nonGroundOutputFile );
185 nonGroundWriter->setOutputProviderKey( QStringLiteral( "gdal" ) );
186 if ( !creationOptions.isEmpty() )
187 {
188 nonGroundWriter->setCreationOptions( creationOptions.split( '|' ) );
189 }
190 nonGroundWriter->setOutputFormat( outputFormat );
191
192 nonGroundDestProvider.reset( nonGroundWriter->createOneBandRaster( mDataType, mLayerWidth, mLayerHeight, mExtent, mCrs ) );
193
194 if ( !nonGroundDestProvider )
195 throw QgsProcessingException( QObject::tr( "Could not create raster output: %1" ).arg( nonGroundOutputFile ) );
196 if ( !nonGroundDestProvider->isValid() )
197 throw QgsProcessingException( QObject::tr( "Could not create raster output %1: %2" ).arg( nonGroundOutputFile, nonGroundDestProvider->error().message( QgsErrorMessage::Text ) ) );
198
199 nonGroundDestProvider->setNoDataValue( 1, mNoData );
200 nonGroundDestProvider->setEditable( true );
201 }
202
205 const int numBlocksX = static_cast<int>( std::ceil( 1.0 * mLayerWidth / blockWidth ) );
206 const int numBlocksY = static_cast<int>( std::ceil( 1.0 * mLayerHeight / blockHeight ) );
207 const int numBlocks = numBlocksX * numBlocksY;
208
209 const int radius = parameterAsInt( parameters, QStringLiteral( "RADIUS" ), context );
210
211 const double terrainSlopePercent = parameterAsDouble( parameters, QStringLiteral( "TERRAIN_SLOPE" ), context ) / 100; //20.0 / 100 * 0.143;
212 const int filterModification = parameterAsEnum( parameters, QStringLiteral( "FILTER_MODIFICATION" ), context );
213 const double standardDeviation = parameterAsDouble( parameters, QStringLiteral( "STANDARD_DEVIATION" ), context );
214
215 // create kernel
216 QVector<double> kernel;
217 kernel.reserve( ( radius * 2 ) * ( radius * 2 ) );
218 int kernelSize = 0;
219 for ( int y = -radius; y <= radius; y++ )
220 {
221 for ( int x = -radius; x <= radius; x++ )
222 {
223 const double distance = std::sqrt( x * x + y * y );
224 if ( distance < radius )
225 {
226 kernelSize++;
227 kernel.push_back( x );
228 kernel.push_back( y );
229 switch ( filterModification )
230 {
231 case 0:
232 kernel.push_back( distance * terrainSlopePercent );
233 break;
234
235 case 1:
236 kernel.push_back( distance * terrainSlopePercent + 1.65 * std::sqrt( 2 * standardDeviation ) );
237 break;
238
239 case 2:
240 {
241 const double dz = distance * terrainSlopePercent - 1.65 * std::sqrt( 2 * standardDeviation );
242 kernel.push_back( dz > 0 ? dz : 0 );
243 break;
244 }
245 }
246 }
247 }
248 }
249
250 QgsRasterIterator iter( mInterface.get(), radius );
251 iter.startRasterRead( 1, mLayerWidth, mLayerHeight, mExtent );
252 int iterLeft = 0;
253 int iterTop = 0;
254 int iterCols = 0;
255 int iterRows = 0;
256 int tileLeft = 0;
257 int tileTop = 0;
258 int tileCols = 0;
259 int tileRows = 0;
260
261 QgsRectangle blockExtent;
262
263 std::unique_ptr<QgsRasterBlock> inputBlock;
264 int blockIndex = 0;
265 while ( iter.readNextRasterPart( 1, iterCols, iterRows, inputBlock, iterLeft, iterTop, &blockExtent, &tileCols, &tileRows, &tileLeft, &tileTop ) )
266 {
267 std::unique_ptr<QgsRasterBlock> outputGroundBlock;
268 if ( groundDestProvider )
269 outputGroundBlock = std::make_unique<QgsRasterBlock>( mDataType, tileCols, tileRows );
270
271 std::unique_ptr<QgsRasterBlock> outputNonGroundBlock;
272 if ( nonGroundDestProvider )
273 outputNonGroundBlock = std::make_unique<QgsRasterBlock>( mDataType, tileCols, tileRows );
274
275 double baseProgress = static_cast<double>( blockIndex ) / numBlocks;
276 feedback->setProgress( 100.0 * baseProgress );
277 blockIndex++;
278 if ( feedback->isCanceled() )
279 break;
280
281 const int tileBoundaryLeft = tileLeft - iterLeft;
282 const int tileBoundaryTop = tileTop - iterTop;
283
284 const double rowProgressStep = 1.0 / numBlocks / tileRows;
285 double rowProgress = 0;
286 for ( int row = tileBoundaryTop; row < tileBoundaryTop + tileRows; row++ )
287 {
288 if ( feedback->isCanceled() )
289 break;
290
291 feedback->setProgress( 100.0 * ( baseProgress + rowProgress ) );
292 rowProgress += rowProgressStep;
293
294 for ( int col = tileBoundaryLeft; col < tileBoundaryLeft + tileCols; col++ )
295 {
296 if ( feedback->isCanceled() )
297 break;
298
299 bool isNoData = false;
300 const double val = inputBlock->valueAndNoData( row, col, isNoData );
301 if ( isNoData )
302 {
303 if ( outputGroundBlock )
304 outputGroundBlock->setValue( row - tileBoundaryTop, col - tileBoundaryLeft, mNoData );
305 if ( outputNonGroundBlock )
306 outputNonGroundBlock->setValue( row - tileBoundaryTop, col - tileBoundaryLeft, mNoData );
307 }
308 else
309 {
310 bool nonGround = false;
311 const double *kernelData = kernel.constData();
312 for ( int i = 0; i < kernelSize; ++i )
313 {
314 const int dx = static_cast<int>( *kernelData++ );
315 const int dy = static_cast<int>( *kernelData++ );
316 const double distance = *kernelData++;
317 const int rCol = col + dx;
318 const int rRow = row + dy;
319 if ( rCol >= 0 && ( rCol < ( iterLeft + iterCols ) ) && rRow >= 0 && ( rRow < ( iterTop + iterRows ) ) )
320 {
321 bool otherIsNoData = false;
322 const double otherVal = inputBlock->valueAndNoData( rRow, rCol, otherIsNoData );
323 if ( !otherIsNoData )
324 {
325 const double dz = val - otherVal;
326 if ( dz > 0 && dz > distance )
327 {
328 nonGround = true;
329 break;
330 }
331 }
332 }
333 }
334 if ( nonGround )
335 {
336 if ( outputGroundBlock )
337 outputGroundBlock->setValue( row - tileBoundaryTop, col - tileBoundaryLeft, mNoData );
338 if ( outputNonGroundBlock )
339 outputNonGroundBlock->setValue( row - tileBoundaryTop, col - tileBoundaryLeft, val );
340 }
341 else
342 {
343 if ( outputGroundBlock )
344 outputGroundBlock->setValue( row - tileBoundaryTop, col - tileBoundaryLeft, val );
345 if ( outputNonGroundBlock )
346 outputNonGroundBlock->setValue( row - tileBoundaryTop, col - tileBoundaryLeft, mNoData );
347 }
348 }
349 }
350 }
351 if ( groundDestProvider )
352 {
353 if ( !groundDestProvider->writeBlock( outputGroundBlock.get(), mBand, tileLeft, tileTop ) )
354 {
355 throw QgsProcessingException( QObject::tr( "Could not write raster block: %1" ).arg( groundDestProvider->error().summary() ) );
356 }
357 }
358 if ( nonGroundDestProvider )
359 {
360 if ( !nonGroundDestProvider->writeBlock( outputNonGroundBlock.get(), mBand, tileLeft, tileTop ) )
361 {
362 throw QgsProcessingException( QObject::tr( "Could not write raster block: %1" ).arg( nonGroundDestProvider->error().summary() ) );
363 }
364 }
365 }
366 if ( groundDestProvider )
367 groundDestProvider->setEditable( false );
368 if ( nonGroundDestProvider )
369 nonGroundDestProvider->setEditable( false );
370
371 QVariantMap outputs;
372 outputs.insert( QStringLiteral( "OUTPUT_GROUND" ), groundOutputFile );
373 outputs.insert( QStringLiteral( "OUTPUT_NONGROUND" ), nonGroundOutputFile );
374 return outputs;
375}
376
377
@ Hidden
Parameter is hidden and should not be shown to users.
@ Advanced
Parameter is an advanced parameter which should be hidden from users by default.
bool isCanceled() const
Tells whether the operation has been canceled already.
Definition qgsfeedback.h:53
void setProgress(double progress)
Sets the current progress for the feedback object.
Definition qgsfeedback.h:61
virtual QgsRectangle extent() const
Returns the extent of the layer.
QgsCoordinateReferenceSystem crs
Definition qgsmaplayer.h:84
Contains information about the context in which a processing algorithm is executed.
Custom exception class for processing related exceptions.
Base class for providing feedback from a processing algorithm.
A raster band parameter for Processing algorithms.
A raster layer parameter for processing algorithms.
QgsRasterDataProvider * clone() const override=0
Clone itself, create deep copy.
virtual bool sourceHasNoDataValue(int bandNo) const
Returns true if source band has no data value.
virtual double sourceNoDataValue(int bandNo) const
Value representing no data value.
Qgis::DataType dataType(int bandNo) const override=0
Returns data type for the band specified by number.
static QString driverForExtension(const QString &extension)
Returns the GDAL driver name for a specified file extension.
Iterator for sequentially processing raster cells.
static const int DEFAULT_MAXIMUM_TILE_WIDTH
Default maximum tile width.
static const int DEFAULT_MAXIMUM_TILE_HEIGHT
Default maximum tile height.
Represents a raster layer.
int height() const
Returns the height of the (unclipped) raster.
int bandCount() const
Returns the number of bands in this layer.
double rasterUnitsPerPixelX() const
Returns the number of raster units per each raster pixel in X axis.
QgsRasterDataProvider * dataProvider() override
Returns the source data provider.
double rasterUnitsPerPixelY() const
Returns the number of raster units per each raster pixel in Y axis.
int width() const
Returns the width of the (unclipped) raster.
A rectangle specified with double values.