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