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