QGIS API Documentation 3.41.0-Master (cea29feecf2)
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#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 std::unique_ptr<QgsProcessingParameterNumber> 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 std::unique_ptr<QgsProcessingParameterNumber> 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 std::unique_ptr<QgsProcessingParameterEnum> 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 std::unique_ptr<QgsProcessingParameterNumber> 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 std::unique_ptr<QgsProcessingParameterString> createOptsParam = std::make_unique<QgsProcessingParameterString>( QStringLiteral( "CREATE_OPTIONS" ), QObject::tr( "Creation options" ), QVariant(), false, true );
89 createOptsParam->setMetadata( QVariantMap( { { QStringLiteral( "widget_wrapper" ), QVariantMap( { { QStringLiteral( "widget_type" ), QStringLiteral( "rasteroptions" ) } } ) } } ) );
90 createOptsParam->setFlags( createOptsParam->flags() | Qgis::ProcessingParameterFlag::Advanced );
91 addParameter( createOptsParam.release() );
92
93 std::unique_ptr<QgsProcessingParameterRasterDestination> outputLayerGroundParam = std::make_unique<QgsProcessingParameterRasterDestination>( QStringLiteral( "OUTPUT_GROUND" ), QObject::tr( "Output layer (ground)" ), QVariant(), true, true );
94 outputLayerGroundParam->setHelp( QObject::tr( "The filtered DEM containing only cells classified as ground." ) );
95 addParameter( outputLayerGroundParam.release() );
96
97 std::unique_ptr<QgsProcessingParameterRasterDestination> outputLayerNonGroundParam = std::make_unique<QgsProcessingParameterRasterDestination>( QStringLiteral( "OUTPUT_NONGROUND" ), QObject::tr( "Output layer (non-ground objects)" ), QVariant(), true, false );
98 outputLayerNonGroundParam->setHelp( QObject::tr( "The non-ground objects removed by the filter." ) );
99 addParameter( outputLayerNonGroundParam.release() );
100}
101
102QgsRasterDtmSlopeBasedFilterAlgorithm *QgsRasterDtmSlopeBasedFilterAlgorithm::createInstance() const
103{
104 return new QgsRasterDtmSlopeBasedFilterAlgorithm();
105}
106
107bool QgsRasterDtmSlopeBasedFilterAlgorithm::prepareAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback * )
108{
109 QgsRasterLayer *layer = parameterAsRasterLayer( parameters, QStringLiteral( "INPUT" ), context );
110 if ( !layer )
111 throw QgsProcessingException( invalidRasterError( parameters, QStringLiteral( "INPUT" ) ) );
112
113 const int band = parameterAsInt( parameters, QStringLiteral( "BAND" ), context );
114
115 mBand = parameterAsInt( parameters, QStringLiteral( "BAND" ), context );
116 if ( mBand < 1 || mBand > layer->bandCount() )
117 throw QgsProcessingException( QObject::tr( "Invalid band number for BAND (%1): Valid values for input raster are 1 to %2" ).arg( mBand ).arg( layer->bandCount() ) );
118
119 mInterface.reset( layer->dataProvider()->clone() );
120 mHasNoDataValue = layer->dataProvider()->sourceHasNoDataValue( band );
121 mLayerWidth = layer->width();
122 mLayerHeight = layer->height();
123 mExtent = layer->extent();
124 mCrs = layer->crs();
125 mRasterUnitsPerPixelX = layer->rasterUnitsPerPixelX();
126 mRasterUnitsPerPixelY = layer->rasterUnitsPerPixelY();
127 mDataType = layer->dataProvider()->dataType( mBand );
128 mNoData = layer->dataProvider()->sourceNoDataValue( mBand );
129 return true;
130}
131
132QVariantMap QgsRasterDtmSlopeBasedFilterAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
133{
134 const QString createOptions = parameterAsString( parameters, QStringLiteral( "CREATE_OPTIONS" ), context ).trimmed();
135 const QString groundOutputFile = parameterAsOutputLayer( parameters, QStringLiteral( "OUTPUT_GROUND" ), context );
136 std::unique_ptr<QgsRasterFileWriter> groundWriter;
137 std::unique_ptr<QgsRasterDataProvider> groundDestProvider;
138
139 if ( !groundOutputFile.isEmpty() )
140 {
141 const QFileInfo fi( groundOutputFile );
142 const QString outputFormat = QgsRasterFileWriter::driverForExtension( fi.suffix() );
143
144 groundWriter = std::make_unique<QgsRasterFileWriter>( groundOutputFile );
145 groundWriter->setOutputProviderKey( QStringLiteral( "gdal" ) );
146 if ( !createOptions.isEmpty() )
147 {
148 groundWriter->setCreateOptions( createOptions.split( '|' ) );
149 }
150 groundWriter->setOutputFormat( outputFormat );
151
152 groundDestProvider.reset( groundWriter->createOneBandRaster( mDataType, mLayerWidth, mLayerHeight, mExtent, mCrs ) );
153
154 if ( !groundDestProvider )
155 throw QgsProcessingException( QObject::tr( "Could not create raster output: %1" ).arg( groundOutputFile ) );
156 if ( !groundDestProvider->isValid() )
157 throw QgsProcessingException( QObject::tr( "Could not create raster output %1: %2" ).arg( groundOutputFile, groundDestProvider->error().message( QgsErrorMessage::Text ) ) );
158
159 groundDestProvider->setNoDataValue( 1, mNoData );
160 groundDestProvider->setEditable( true );
161 }
162
163 const QString nonGroundOutputFile = parameterAsOutputLayer( parameters, QStringLiteral( "OUTPUT_NONGROUND" ), context );
164 std::unique_ptr<QgsRasterFileWriter> nonGroundWriter;
165 std::unique_ptr<QgsRasterDataProvider> nonGroundDestProvider;
166
167 if ( !nonGroundOutputFile.isEmpty() )
168 {
169 const QFileInfo fi( groundOutputFile );
170 const QString outputFormat = QgsRasterFileWriter::driverForExtension( fi.suffix() );
171
172 nonGroundWriter = std::make_unique<QgsRasterFileWriter>( nonGroundOutputFile );
173 nonGroundWriter->setOutputProviderKey( QStringLiteral( "gdal" ) );
174 if ( !createOptions.isEmpty() )
175 {
176 nonGroundWriter->setCreateOptions( createOptions.split( '|' ) );
177 }
178 nonGroundWriter->setOutputFormat( outputFormat );
179
180 nonGroundDestProvider.reset( nonGroundWriter->createOneBandRaster( mDataType, mLayerWidth, mLayerHeight, mExtent, mCrs ) );
181
182 if ( !nonGroundDestProvider )
183 throw QgsProcessingException( QObject::tr( "Could not create raster output: %1" ).arg( nonGroundOutputFile ) );
184 if ( !nonGroundDestProvider->isValid() )
185 throw QgsProcessingException( QObject::tr( "Could not create raster output %1: %2" ).arg( nonGroundOutputFile, nonGroundDestProvider->error().message( QgsErrorMessage::Text ) ) );
186
187 nonGroundDestProvider->setNoDataValue( 1, mNoData );
188 nonGroundDestProvider->setEditable( true );
189 }
190
193 const int numBlocksX = static_cast<int>( std::ceil( 1.0 * mLayerWidth / blockWidth ) );
194 const int numBlocksY = static_cast<int>( std::ceil( 1.0 * mLayerHeight / blockHeight ) );
195 const int numBlocks = numBlocksX * numBlocksY;
196
197 const int radius = parameterAsInt( parameters, QStringLiteral( "RADIUS" ), context );
198
199 const double terrainSlopePercent = parameterAsDouble( parameters, QStringLiteral( "TERRAIN_SLOPE" ), context ) / 100; //20.0 / 100 * 0.143;
200 const int filterModification = parameterAsEnum( parameters, QStringLiteral( "FILTER_MODIFICATION" ), context );
201 const double standardDeviation = parameterAsDouble( parameters, QStringLiteral( "STANDARD_DEVIATION" ), context );
202
203 // create kernel
204 QVector<double> kernel;
205 kernel.reserve( ( radius * 2 ) * ( radius * 2 ) );
206 int kernelSize = 0;
207 for ( int y = -radius; y <= radius; y++ )
208 {
209 for ( int x = -radius; x <= radius; x++ )
210 {
211 const double distance = std::sqrt( x * x + y * y );
212 if ( distance < radius )
213 {
214 kernelSize++;
215 kernel.push_back( x );
216 kernel.push_back( y );
217 switch ( filterModification )
218 {
219 case 0:
220 kernel.push_back( distance * terrainSlopePercent );
221 break;
222
223 case 1:
224 kernel.push_back( distance * terrainSlopePercent + 1.65 * std::sqrt( 2 * standardDeviation ) );
225 break;
226
227 case 2:
228 {
229 const double dz = distance * terrainSlopePercent - 1.65 * std::sqrt( 2 * standardDeviation );
230 kernel.push_back( dz > 0 ? dz : 0 );
231 break;
232 }
233 }
234 }
235 }
236 }
237
238 QgsRasterIterator iter( mInterface.get(), radius );
239 iter.startRasterRead( 1, mLayerWidth, mLayerHeight, mExtent );
240 int iterLeft = 0;
241 int iterTop = 0;
242 int iterCols = 0;
243 int iterRows = 0;
244 int tileLeft = 0;
245 int tileTop = 0;
246 int tileCols = 0;
247 int tileRows = 0;
248
249 QgsRectangle blockExtent;
250
251 std::unique_ptr<QgsRasterBlock> inputBlock;
252 int blockIndex = 0;
253 while ( iter.readNextRasterPart( 1, iterCols, iterRows, inputBlock, iterLeft, iterTop, &blockExtent, &tileCols, &tileRows, &tileLeft, &tileTop ) )
254 {
255 std::unique_ptr<QgsRasterBlock> outputGroundBlock;
256 if ( groundDestProvider )
257 outputGroundBlock = std::make_unique<QgsRasterBlock>( mDataType, tileCols, tileRows );
258
259 std::unique_ptr<QgsRasterBlock> outputNonGroundBlock;
260 if ( nonGroundDestProvider )
261 outputNonGroundBlock = std::make_unique<QgsRasterBlock>( mDataType, tileCols, tileRows );
262
263 double baseProgress = static_cast<double>( blockIndex ) / numBlocks;
264 feedback->setProgress( 100.0 * baseProgress );
265 blockIndex++;
266 if ( feedback->isCanceled() )
267 break;
268
269 const int tileBoundaryLeft = tileLeft - iterLeft;
270 const int tileBoundaryTop = tileTop - iterTop;
271
272 const double rowProgressStep = 1.0 / numBlocks / tileRows;
273 double rowProgress = 0;
274 for ( int row = tileBoundaryTop; row < tileBoundaryTop + tileRows; row++ )
275 {
276 if ( feedback->isCanceled() )
277 break;
278
279 feedback->setProgress( 100.0 * ( baseProgress + rowProgress ) );
280 rowProgress += rowProgressStep;
281
282 for ( int col = tileBoundaryLeft; col < tileBoundaryLeft + tileCols; col++ )
283 {
284 if ( feedback->isCanceled() )
285 break;
286
287 bool isNoData = false;
288 const double val = inputBlock->valueAndNoData( row, col, isNoData );
289 if ( isNoData )
290 {
291 if ( outputGroundBlock )
292 outputGroundBlock->setValue( row - tileBoundaryTop, col - tileBoundaryLeft, mNoData );
293 if ( outputNonGroundBlock )
294 outputNonGroundBlock->setValue( row - tileBoundaryTop, col - tileBoundaryLeft, mNoData );
295 }
296 else
297 {
298 bool nonGround = false;
299 const double *kernelData = kernel.constData();
300 for ( int i = 0; i < kernelSize; ++i )
301 {
302 const int dx = static_cast<int>( *kernelData++ );
303 const int dy = static_cast<int>( *kernelData++ );
304 const double distance = *kernelData++;
305 const int rCol = col + dx;
306 const int rRow = row + dy;
307 if ( rCol >= 0 && ( rCol < ( iterLeft + iterCols ) ) && rRow >= 0 && ( rRow < ( iterTop + iterRows ) ) )
308 {
309 bool otherIsNoData = false;
310 const double otherVal = inputBlock->valueAndNoData( rRow, rCol, otherIsNoData );
311 if ( !otherIsNoData )
312 {
313 const double dz = val - otherVal;
314 if ( dz > 0 && dz > distance )
315 {
316 nonGround = true;
317 break;
318 }
319 }
320 }
321 }
322 if ( nonGround )
323 {
324 if ( outputGroundBlock )
325 outputGroundBlock->setValue( row - tileBoundaryTop, col - tileBoundaryLeft, mNoData );
326 if ( outputNonGroundBlock )
327 outputNonGroundBlock->setValue( row - tileBoundaryTop, col - tileBoundaryLeft, val );
328 }
329 else
330 {
331 if ( outputGroundBlock )
332 outputGroundBlock->setValue( row - tileBoundaryTop, col - tileBoundaryLeft, val );
333 if ( outputNonGroundBlock )
334 outputNonGroundBlock->setValue( row - tileBoundaryTop, col - tileBoundaryLeft, mNoData );
335 }
336 }
337 }
338 }
339 if ( groundDestProvider )
340 groundDestProvider->writeBlock( outputGroundBlock.get(), mBand, tileLeft, tileTop );
341 if ( nonGroundDestProvider )
342 nonGroundDestProvider->writeBlock( outputNonGroundBlock.get(), mBand, tileLeft, tileTop );
343 }
344 if ( groundDestProvider )
345 groundDestProvider->setEditable( false );
346 if ( nonGroundDestProvider )
347 nonGroundDestProvider->setEditable( false );
348
349 QVariantMap outputs;
350 outputs.insert( QStringLiteral( "OUTPUT_GROUND" ), groundOutputFile );
351 outputs.insert( QStringLiteral( "OUTPUT_NONGROUND" ), nonGroundOutputFile );
352 return outputs;
353}
354
355
@ 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:83
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.