QGIS API Documentation 3.99.0-Master (357b655ed83)
Loading...
Searching...
No Matches
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
20#include <algorithm>
21
22#include "qgsrasterfilewriter.h"
23
24#include <QString>
25
26using namespace Qt::StringLiterals;
27
29
30QString QgsRasterDtmSlopeBasedFilterAlgorithm::name() const
31{
32 return u"dtmslopebasedfilter"_s;
33}
34
35QString QgsRasterDtmSlopeBasedFilterAlgorithm::displayName() const
36{
37 return QObject::tr( "DTM filter (slope-based)" );
38}
39
40QStringList QgsRasterDtmSlopeBasedFilterAlgorithm::tags() const
41{
42 return QObject::tr( "dem,filter,slope,dsm,dtm,terrain" ).split( ',' );
43}
44
45QString QgsRasterDtmSlopeBasedFilterAlgorithm::group() const
46{
47 return QObject::tr( "Raster terrain analysis" );
48}
49
50QString QgsRasterDtmSlopeBasedFilterAlgorithm::groupId() const
51{
52 return u"rasterterrainanalysis"_s;
53}
54
55QString QgsRasterDtmSlopeBasedFilterAlgorithm::shortHelpString() const
56{
57 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"
58 "The tool uses concepts as described by Vosselman (2000) and is based on the assumption that a large height difference between two nearby "
59 "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 "
60 "the distance between the two cells decreases. Therefore the filter defines a maximum height difference (<i>dz_max</i>) between two cells as a "
61 "function of the distance (<i>d</i>) between the cells (<i>dz_max( d ) = d</i>).\n\n"
62 "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 "
63 "maximum height difference at the distance between these two cells.\n\n"
64 "The approximate terrain slope (<i>s</i>) parameter is used to modify the filter function to match the overall slope in the study "
65 "area (<i>dz_max( d ) = d * s</i>).\n\n"
66 "A 5 % confidence interval (<i>ci = 1.65 * sqrt( 2 * stddev )</i>) may be used to modify the filter function even further by either "
67 "relaxing (<i>dz_max( d ) = d * s + ci</i>) or amplifying (<i>dz_max( d ) = d * s - ci</i>) the filter criterium.\n\n"
68 "References: Vosselman, G. (2000): Slope based filtering of laser altimetry data. IAPRS, Vol. XXXIII, Part B3, Amsterdam, The Netherlands, 935-942\n\n"
69 "This algorithm is a port of the SAGA 'DTM Filter (slope-based)' tool." );
70}
71
72QString QgsRasterDtmSlopeBasedFilterAlgorithm::shortDescription() const
73{
74 return QObject::tr( "Filters a Digital Elevation Model in order to classify its cells into ground and object (non-ground) cells." );
75}
76
77void QgsRasterDtmSlopeBasedFilterAlgorithm::initAlgorithm( const QVariantMap & )
78{
79 addParameter( new QgsProcessingParameterRasterLayer( u"INPUT"_s, QObject::tr( "Input layer" ) ) );
80
81 addParameter( new QgsProcessingParameterBand( u"BAND"_s, QObject::tr( "Band number" ), 1, u"INPUT"_s ) );
82
83 auto radiusParam = std::make_unique<QgsProcessingParameterNumber>( u"RADIUS"_s, QObject::tr( "Kernel radius (pixels)" ), Qgis::ProcessingNumberParameterType::Integer, 5, false, 1, 1000 );
84 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." ) );
85 addParameter( radiusParam.release() );
86
87 auto terrainSlopeParam = std::make_unique<QgsProcessingParameterNumber>( u"TERRAIN_SLOPE"_s, QObject::tr( "Terrain slope (%, pixel size/vertical units)" ), Qgis::ProcessingNumberParameterType::Double, 30, false, 0, 1000 );
88 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." ) );
89 addParameter( terrainSlopeParam.release() );
90
91 auto filterModificationParam = std::make_unique<QgsProcessingParameterEnum>( u"FILTER_MODIFICATION"_s, QObject::tr( "Filter modification" ), QStringList { QObject::tr( "None" ), QObject::tr( "Relax filter" ), QObject::tr( "Amplify" ) }, false, 0 );
92 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." ) );
93 addParameter( filterModificationParam.release() );
94
95 auto stDevParam = std::make_unique<QgsProcessingParameterNumber>( u"STANDARD_DEVIATION"_s, QObject::tr( "Standard deviation" ), Qgis::ProcessingNumberParameterType::Double, 0.1, false, 0, 1000 );
96 stDevParam->setHelp( QObject::tr( "The standard deviation used to calculate a 5% confidence interval applied to the height threshold." ) );
97 addParameter( stDevParam.release() );
98
99 // backwards compatibility parameter
100 // TODO QGIS 5: remove parameter and related logic
101 auto createOptsParam = std::make_unique<QgsProcessingParameterString>( u"CREATE_OPTIONS"_s, QObject::tr( "Creation options" ), QVariant(), false, true );
102 createOptsParam->setMetadata( QVariantMap( { { u"widget_wrapper"_s, QVariantMap( { { u"widget_type"_s, u"rasteroptions"_s } } ) } } ) );
103 createOptsParam->setFlags( createOptsParam->flags() | Qgis::ProcessingParameterFlag::Hidden );
104 addParameter( createOptsParam.release() );
105
106 auto creationOptsParam = std::make_unique<QgsProcessingParameterString>( u"CREATION_OPTIONS"_s, QObject::tr( "Creation options" ), QVariant(), false, true );
107 creationOptsParam->setMetadata( QVariantMap( { { u"widget_wrapper"_s, QVariantMap( { { u"widget_type"_s, u"rasteroptions"_s } } ) } } ) );
108 creationOptsParam->setFlags( creationOptsParam->flags() | Qgis::ProcessingParameterFlag::Advanced );
109 addParameter( creationOptsParam.release() );
110
111 auto outputLayerGroundParam = std::make_unique<QgsProcessingParameterRasterDestination>( u"OUTPUT_GROUND"_s, QObject::tr( "Output layer (ground)" ), QVariant(), true, true );
112 outputLayerGroundParam->setHelp( QObject::tr( "The filtered DEM containing only cells classified as ground." ) );
113 addParameter( outputLayerGroundParam.release() );
114
115 auto outputLayerNonGroundParam = std::make_unique<QgsProcessingParameterRasterDestination>( u"OUTPUT_NONGROUND"_s, QObject::tr( "Output layer (non-ground objects)" ), QVariant(), true, false );
116 outputLayerNonGroundParam->setHelp( QObject::tr( "The non-ground objects removed by the filter." ) );
117 addParameter( outputLayerNonGroundParam.release() );
118}
119
120QgsRasterDtmSlopeBasedFilterAlgorithm *QgsRasterDtmSlopeBasedFilterAlgorithm::createInstance() const
121{
122 return new QgsRasterDtmSlopeBasedFilterAlgorithm();
123}
124
125bool QgsRasterDtmSlopeBasedFilterAlgorithm::prepareAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback * )
126{
127 QgsRasterLayer *layer = parameterAsRasterLayer( parameters, u"INPUT"_s, context );
128 if ( !layer )
129 throw QgsProcessingException( invalidRasterError( parameters, u"INPUT"_s ) );
130
131 const int band = parameterAsInt( parameters, u"BAND"_s, context );
132
133 mBand = parameterAsInt( parameters, u"BAND"_s, context );
134 if ( mBand < 1 || mBand > layer->bandCount() )
135 throw QgsProcessingException( QObject::tr( "Invalid band number for BAND (%1): Valid values for input raster are 1 to %2" ).arg( mBand ).arg( layer->bandCount() ) );
136
137 mInterface.reset( layer->dataProvider()->clone() );
138 mHasNoDataValue = layer->dataProvider()->sourceHasNoDataValue( band );
139 mLayerWidth = layer->width();
140 mLayerHeight = layer->height();
141 mExtent = layer->extent();
142 mCrs = layer->crs();
143 mRasterUnitsPerPixelX = layer->rasterUnitsPerPixelX();
144 mRasterUnitsPerPixelY = layer->rasterUnitsPerPixelY();
145 mDataType = layer->dataProvider()->dataType( mBand );
146 mNoData = layer->dataProvider()->sourceNoDataValue( mBand );
147 return true;
148}
149
150QVariantMap QgsRasterDtmSlopeBasedFilterAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
151{
152 QString creationOptions = parameterAsString( parameters, u"CREATION_OPTIONS"_s, context ).trimmed();
153 // handle backwards compatibility parameter CREATE_OPTIONS
154 const QString optionsString = parameterAsString( parameters, u"CREATE_OPTIONS"_s, context );
155 if ( !optionsString.isEmpty() )
156 creationOptions = optionsString;
157
158 const QString groundOutputFile = parameterAsOutputLayer( parameters, u"OUTPUT_GROUND"_s, context );
159 std::unique_ptr<QgsRasterFileWriter> groundWriter;
160 std::unique_ptr<QgsRasterDataProvider> groundDestProvider;
161
162 if ( !groundOutputFile.isEmpty() )
163 {
164 const QString outputFormat = parameterAsOutputRasterFormat( parameters, u"OUTPUT_GROUND"_s, context );
165
166 groundWriter = std::make_unique<QgsRasterFileWriter>( groundOutputFile );
167 groundWriter->setOutputProviderKey( u"gdal"_s );
168 if ( !creationOptions.isEmpty() )
169 {
170 groundWriter->setCreationOptions( creationOptions.split( '|' ) );
171 }
172 groundWriter->setOutputFormat( outputFormat );
173
174 groundDestProvider.reset( groundWriter->createOneBandRaster( mDataType, mLayerWidth, mLayerHeight, mExtent, mCrs ) );
175
176 if ( !groundDestProvider )
177 throw QgsProcessingException( QObject::tr( "Could not create raster output: %1" ).arg( groundOutputFile ) );
178 if ( !groundDestProvider->isValid() )
179 throw QgsProcessingException( QObject::tr( "Could not create raster output %1: %2" ).arg( groundOutputFile, groundDestProvider->error().message( QgsErrorMessage::Text ) ) );
180
181 groundDestProvider->setNoDataValue( 1, mNoData );
182 groundDestProvider->setEditable( true );
183 }
184
185 const QString nonGroundOutputFile = parameterAsOutputLayer( parameters, u"OUTPUT_NONGROUND"_s, context );
186 std::unique_ptr<QgsRasterFileWriter> nonGroundWriter;
187 std::unique_ptr<QgsRasterDataProvider> nonGroundDestProvider;
188
189 if ( !nonGroundOutputFile.isEmpty() )
190 {
191 const QString outputFormat = parameterAsOutputRasterFormat( parameters, u"OUTPUT_NONGROUND"_s, context );
192
193 nonGroundWriter = std::make_unique<QgsRasterFileWriter>( nonGroundOutputFile );
194 nonGroundWriter->setOutputProviderKey( u"gdal"_s );
195 if ( !creationOptions.isEmpty() )
196 {
197 nonGroundWriter->setCreationOptions( creationOptions.split( '|' ) );
198 }
199 nonGroundWriter->setOutputFormat( outputFormat );
200
201 nonGroundDestProvider.reset( nonGroundWriter->createOneBandRaster( mDataType, mLayerWidth, mLayerHeight, mExtent, mCrs ) );
202
203 if ( !nonGroundDestProvider )
204 throw QgsProcessingException( QObject::tr( "Could not create raster output: %1" ).arg( nonGroundOutputFile ) );
205 if ( !nonGroundDestProvider->isValid() )
206 throw QgsProcessingException( QObject::tr( "Could not create raster output %1: %2" ).arg( nonGroundOutputFile, nonGroundDestProvider->error().message( QgsErrorMessage::Text ) ) );
207
208 nonGroundDestProvider->setNoDataValue( 1, mNoData );
209 nonGroundDestProvider->setEditable( true );
210 }
211
214 const int numBlocksX = static_cast<int>( std::ceil( 1.0 * mLayerWidth / blockWidth ) );
215 const int numBlocksY = static_cast<int>( std::ceil( 1.0 * mLayerHeight / blockHeight ) );
216 const int numBlocks = numBlocksX * numBlocksY;
217
218 const int radius = parameterAsInt( parameters, u"RADIUS"_s, context );
219
220 const double terrainSlopePercent = parameterAsDouble( parameters, u"TERRAIN_SLOPE"_s, context ) / 100; //20.0 / 100 * 0.143;
221 const int filterModification = parameterAsEnum( parameters, u"FILTER_MODIFICATION"_s, context );
222 const double standardDeviation = parameterAsDouble( parameters, u"STANDARD_DEVIATION"_s, context );
223
224 // create kernel
225 QVector<double> kernel;
226 kernel.reserve( ( radius * 2 ) * ( radius * 2 ) );
227 int kernelSize = 0;
228 for ( int y = -radius; y <= radius; y++ )
229 {
230 for ( int x = -radius; x <= radius; x++ )
231 {
232 const double distance = std::sqrt( x * x + y * y );
233 if ( distance < radius )
234 {
235 kernelSize++;
236 kernel.push_back( x );
237 kernel.push_back( y );
238 switch ( filterModification )
239 {
240 case 0:
241 kernel.push_back( distance * terrainSlopePercent );
242 break;
243
244 case 1:
245 kernel.push_back( distance * terrainSlopePercent + 1.65 * std::sqrt( 2 * standardDeviation ) );
246 break;
247
248 case 2:
249 {
250 const double dz = distance * terrainSlopePercent - 1.65 * std::sqrt( 2 * standardDeviation );
251 kernel.push_back( dz > 0 ? dz : 0 );
252 break;
253 }
254 }
255 }
256 }
257 }
258
259 QgsRasterIterator iter( mInterface.get(), radius );
260 iter.startRasterRead( 1, mLayerWidth, mLayerHeight, mExtent );
261 int iterLeft = 0;
262 int iterTop = 0;
263 int iterCols = 0;
264 int iterRows = 0;
265 int tileLeft = 0;
266 int tileTop = 0;
267 int tileCols = 0;
268 int tileRows = 0;
269
270 QgsRectangle blockExtent;
271
272 const bool hasGroundsReportsDuringClose = groundDestProvider && groundDestProvider->hasReportsDuringClose();
273 const bool hasNonGroundsReportsDuringClose = nonGroundDestProvider && nonGroundDestProvider->hasReportsDuringClose();
274 const double maxProgressDuringBlockWriting = 100.0 / ( 1 + ( hasGroundsReportsDuringClose ? 1 : 0 ) + ( hasNonGroundsReportsDuringClose ? 1 : 0 ) );
275
276 std::unique_ptr<QgsRasterBlock> inputBlock;
277 int blockIndex = 0;
278 while ( iter.readNextRasterPart( 1, iterCols, iterRows, inputBlock, iterLeft, iterTop, &blockExtent, &tileCols, &tileRows, &tileLeft, &tileTop ) )
279 {
280 std::unique_ptr<QgsRasterBlock> outputGroundBlock;
281 if ( groundDestProvider )
282 outputGroundBlock = std::make_unique<QgsRasterBlock>( mDataType, tileCols, tileRows );
283
284 std::unique_ptr<QgsRasterBlock> outputNonGroundBlock;
285 if ( nonGroundDestProvider )
286 outputNonGroundBlock = std::make_unique<QgsRasterBlock>( mDataType, tileCols, tileRows );
287
288 double baseProgress = static_cast<double>( blockIndex ) / numBlocks;
289 feedback->setProgress( maxProgressDuringBlockWriting * baseProgress );
290 blockIndex++;
291 if ( feedback->isCanceled() )
292 break;
293
294 const int tileBoundaryLeft = tileLeft - iterLeft;
295 const int tileBoundaryTop = tileTop - iterTop;
296
297 const double rowProgressStep = 1.0 / numBlocks / tileRows;
298 double rowProgress = 0;
299 for ( int row = tileBoundaryTop; row < tileBoundaryTop + tileRows; row++ )
300 {
301 if ( feedback->isCanceled() )
302 break;
303
304 feedback->setProgress( maxProgressDuringBlockWriting * ( baseProgress + rowProgress ) );
305 rowProgress += rowProgressStep;
306
307 for ( int col = tileBoundaryLeft; col < tileBoundaryLeft + tileCols; col++ )
308 {
309 if ( feedback->isCanceled() )
310 break;
311
312 bool isNoData = false;
313 const double val = inputBlock->valueAndNoData( row, col, isNoData );
314 if ( isNoData )
315 {
316 if ( outputGroundBlock )
317 outputGroundBlock->setValue( row - tileBoundaryTop, col - tileBoundaryLeft, mNoData );
318 if ( outputNonGroundBlock )
319 outputNonGroundBlock->setValue( row - tileBoundaryTop, col - tileBoundaryLeft, mNoData );
320 }
321 else
322 {
323 bool nonGround = false;
324 const double *kernelData = kernel.constData();
325 for ( int i = 0; i < kernelSize; ++i )
326 {
327 const int dx = static_cast<int>( *kernelData++ );
328 const int dy = static_cast<int>( *kernelData++ );
329 const double distance = *kernelData++;
330 const int rCol = col + dx;
331 const int rRow = row + dy;
332 if ( rCol >= 0 && ( rCol < ( iterLeft + iterCols ) ) && rRow >= 0 && ( rRow < ( iterTop + iterRows ) ) )
333 {
334 bool otherIsNoData = false;
335 const double otherVal = inputBlock->valueAndNoData( rRow, rCol, otherIsNoData );
336 if ( !otherIsNoData )
337 {
338 const double dz = val - otherVal;
339 if ( dz > 0 && dz > distance )
340 {
341 nonGround = true;
342 break;
343 }
344 }
345 }
346 }
347 if ( nonGround )
348 {
349 if ( outputGroundBlock )
350 outputGroundBlock->setValue( row - tileBoundaryTop, col - tileBoundaryLeft, mNoData );
351 if ( outputNonGroundBlock )
352 outputNonGroundBlock->setValue( row - tileBoundaryTop, col - tileBoundaryLeft, val );
353 }
354 else
355 {
356 if ( outputGroundBlock )
357 outputGroundBlock->setValue( row - tileBoundaryTop, col - tileBoundaryLeft, val );
358 if ( outputNonGroundBlock )
359 outputNonGroundBlock->setValue( row - tileBoundaryTop, col - tileBoundaryLeft, mNoData );
360 }
361 }
362 }
363 }
364 if ( groundDestProvider )
365 {
366 if ( !groundDestProvider->writeBlock( outputGroundBlock.get(), mBand, tileLeft, tileTop ) )
367 {
368 throw QgsProcessingException( QObject::tr( "Could not write raster block: %1" ).arg( groundDestProvider->error().summary() ) );
369 }
370 }
371 if ( nonGroundDestProvider )
372 {
373 if ( !nonGroundDestProvider->writeBlock( outputNonGroundBlock.get(), mBand, tileLeft, tileTop ) )
374 {
375 throw QgsProcessingException( QObject::tr( "Could not write raster block: %1" ).arg( nonGroundDestProvider->error().summary() ) );
376 }
377 }
378 }
379
380 double lastProgress = maxProgressDuringBlockWriting;
381
382 if ( groundDestProvider )
383 {
384 groundDestProvider->setEditable( false );
385 if ( feedback && hasGroundsReportsDuringClose )
386 {
387 const double nextProgress = hasNonGroundsReportsDuringClose ? 100.0 * 2 / 3 : 100.0;
388 std::unique_ptr<QgsFeedback> scaledFeedback( QgsFeedback::createScaledFeedback( feedback, lastProgress, nextProgress ) );
389 if ( !groundDestProvider->closeWithProgress( scaledFeedback.get() ) )
390 {
391 if ( feedback->isCanceled() )
392 return {};
393 throw QgsProcessingException( QObject::tr( "Could not write raster dataset" ) );
394 }
395 lastProgress = nextProgress;
396 }
397 }
398 if ( nonGroundDestProvider )
399 {
400 nonGroundDestProvider->setEditable( false );
401 if ( feedback && hasNonGroundsReportsDuringClose )
402 {
403 std::unique_ptr<QgsFeedback> scaledFeedback( QgsFeedback::createScaledFeedback( feedback, lastProgress, 100.0 ) );
404 if ( !nonGroundDestProvider->closeWithProgress( scaledFeedback.get() ) )
405 {
406 if ( feedback->isCanceled() )
407 return {};
408 throw QgsProcessingException( QObject::tr( "Could not write raster dataset" ) );
409 }
410 }
411 }
412
413 QVariantMap outputs;
414 outputs.insert( u"OUTPUT_GROUND"_s, groundOutputFile );
415 outputs.insert( u"OUTPUT_NONGROUND"_s, nonGroundOutputFile );
416 return outputs;
417}
418
419
@ Hidden
Parameter is hidden and should not be shown to users.
Definition qgis.h:3835
@ Advanced
Parameter is an advanced parameter which should be hidden from users by default.
Definition qgis.h:3834
@ Double
Double/float values.
Definition qgis.h:3875
bool isCanceled() const
Tells whether the operation has been canceled already.
Definition qgsfeedback.h:55
void setProgress(double progress)
Sets the current progress for the feedback object.
Definition qgsfeedback.h:63
static std::unique_ptr< QgsFeedback > createScaledFeedback(QgsFeedback *parentFeedback, double startPercentage, double endPercentage)
Returns a feedback object whose [0, 100] progression range will be mapped to parentFeedback [startPer...
virtual QgsRectangle extent() const
Returns the extent of the layer.
QgsCoordinateReferenceSystem crs
Definition qgsmaplayer.h:90
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.
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.