24using namespace Qt::StringLiterals;
33 Vector2D(
double x = 0,
double y = 0 )
38 double angleBetweenCos(
const Vector2D &other )
const
40 const double dot = x * other.x + y * other.y + 1.0;
41 const double magSelf = ( x * x + y * y + 1.0 );
42 const double magOther = ( other.x * other.x + other.y * other.y + 1.0 );
43 return dot / std::sqrt( magSelf * magOther );
47QString QgsRasterFeaturePreservingSmoothingAlgorithm::name()
const
49 return u
"rasterfeaturepreservingsmoothing"_s;
52QString QgsRasterFeaturePreservingSmoothingAlgorithm::displayName()
const
54 return QObject::tr(
"Feature preserving DEM smoothing" );
57QStringList QgsRasterFeaturePreservingSmoothingAlgorithm::tags()
const
59 return QObject::tr(
"smooth,filter,denoise,fpdems,blur" ).split(
',' );
62QString QgsRasterFeaturePreservingSmoothingAlgorithm::group()
const
64 return QObject::tr(
"Raster analysis" );
67QString QgsRasterFeaturePreservingSmoothingAlgorithm::groupId()
const
69 return u
"rasteranalysis"_s;
72QString QgsRasterFeaturePreservingSmoothingAlgorithm::shortHelpString()
const
74 return QObject::tr(
"This algorithm applies the Feature-Preserving DEM Smoothing (FPDEMS) method, as described by Lindsay et al. (2019).\n\n"
75 "It is effective at removing surface roughness from Digital Elevation Models (DEMs) without significantly altering sharp features such as breaks-in-slope, stream banks, or terrace scarps. "
76 "This makes it superior to standard low-pass filters (e.g., mean, median, Gaussian) or resampling, which often blur distinct topographic features.\n\n"
77 "The algorithm works in three steps:\n"
78 "1. Calculating surface normal 3D vectors for each grid cell.\n"
79 "2. Smoothing the normal vector field using a filter that applies more weight to neighbors with similar surface normals (preserving edges).\n"
80 "3. Iteratively updating the elevations in the DEM to match the smoothed normal field.\n\n"
81 "References: Lindsay, John et al (2019): LiDAR DEM Smoothing and the Preservation of Drainage Features. Remote Sensing, Vol. 11, Issue 16, https://doi.org/10.3390/rs11161926"
85QString QgsRasterFeaturePreservingSmoothingAlgorithm::shortDescription()
const
87 return QObject::tr(
"Smooths a DEM while preserving topographic features." );
90void QgsRasterFeaturePreservingSmoothingAlgorithm::initAlgorithm(
const QVariantMap & )
97 radiusParam->setHelp( QObject::tr(
"Radius of the filter kernel. A radius of 5 results in an 11x11 kernel." ) );
98 addParameter( radiusParam.release() );
100 auto thresholdParam = std::make_unique<QgsProcessingParameterNumber>( u
"THRESHOLD"_s, QObject::tr(
"Normal difference threshold (degrees)" ),
Qgis::ProcessingNumberParameterType::Double, 15.0,
false, 0.0, 90.0 );
101 thresholdParam->setHelp( QObject::tr(
"Maximum angular difference (in degrees) between the normal vector of the center cell and a neighbor for the neighbor to be included in the filter. Higher values result in more neighbors being included, producing smoother surfaces. A range of 10-20 degrees is typically optimal." ) );
102 addParameter( thresholdParam.release() );
105 iterParam->setHelp( QObject::tr(
"Number of times the smoothing process (elevation update) is repeated. Increasing this value from the default of 3 will result in significantly greater smoothing." ) );
106 addParameter( iterParam.release() );
109 maxDiffParam->setHelp( QObject::tr(
"The allowed maximum height change of any cell in one iteration. If the calculated change exceeds this value, the elevation remains unchanged. This prevents excessive deviation from the original surface." ) );
110 addParameter( maxDiffParam.release() );
113 zFactorParam->setHelp( QObject::tr(
"Multiplication factor to convert vertical Z units to horizontal XY units." ) );
115 zFactorParam->setMetadata(
116 { QVariantMap( { { u
"widget_wrapper"_s, QVariantMap( { { u
"decimals"_s, 12 } } ) } } )
119 addParameter( zFactorParam.release() );
121 auto creationOptsParam = std::make_unique<QgsProcessingParameterString>( u
"CREATION_OPTIONS"_s, QObject::tr(
"Creation options" ), QVariant(),
false,
true );
122 creationOptsParam->setMetadata( QVariantMap( { { u
"widget_wrapper"_s, QVariantMap( { { u
"widget_type"_s, u
"rasteroptions"_s } } ) } } ) );
124 addParameter( creationOptsParam.release() );
126 auto outputLayerParam = std::make_unique<QgsProcessingParameterRasterDestination>( u
"OUTPUT"_s, QObject::tr(
"Output layer" ) );
127 addParameter( outputLayerParam.release() );
130QgsRasterFeaturePreservingSmoothingAlgorithm *QgsRasterFeaturePreservingSmoothingAlgorithm::createInstance()
const
132 return new QgsRasterFeaturePreservingSmoothingAlgorithm();
137 QgsRasterLayer *layer = parameterAsRasterLayer( parameters, u
"INPUT"_s, context );
141 mBand = parameterAsInt( parameters, u
"BAND"_s, context );
142 if ( mBand < 1 || mBand > layer->
bandCount() )
143 throw QgsProcessingException( QObject::tr(
"Invalid band number for BAND (%1): Valid values for input raster are 1 to %2" ).arg( mBand ).arg( layer->
bandCount() ) );
146 mLayerWidth = layer->
width();
147 mLayerHeight = layer->
height();
148 mExtent = layer->
extent();
159 const int radius = parameterAsInt( parameters, u
"RADIUS"_s, context );
161 const double thresholdDegrees = parameterAsDouble( parameters, u
"THRESHOLD"_s, context );
162 const double cosThreshold = std::cos( thresholdDegrees * M_PI / 180.0 );
164 const int iterations = parameterAsInt( parameters, u
"ITERATIONS"_s, context );
165 const bool hasMaxElevChange = parameters.value( u
"MAX_ELEVATION_CHANGE"_s ).isValid();
166 const double maxElevChange = hasMaxElevChange ? parameterAsDouble( parameters, u
"MAX_ELEVATION_CHANGE"_s, context ) : 0;
167 const double zFactor = parameterAsDouble( parameters, u
"Z_FACTOR"_s, context );
169 const double oneOver8ResX = 1 / ( 8.0 * mRasterUnitsPerPixelX );
170 const double oneOver8ResY = 1 / ( 8.0 * mRasterUnitsPerPixelY );
172 const QString creationOptions = parameterAsString( parameters, u
"CREATION_OPTIONS"_s, context ).trimmed();
173 const QString outputFile = parameterAsOutputLayer( parameters, u
"OUTPUT"_s, context );
174 const QString outputFormat = parameterAsOutputRasterFormat( parameters, u
"OUTPUT"_s, context );
176 auto outputWriter = std::make_unique<QgsRasterFileWriter>( outputFile );
177 outputWriter->setOutputProviderKey( u
"gdal"_s );
178 if ( !creationOptions.isEmpty() )
180 outputWriter->setCreationOptions( creationOptions.split(
'|' ) );
182 outputWriter->setOutputFormat( outputFormat );
184 std::unique_ptr<QgsRasterDataProvider> destProvider( outputWriter->createOneBandRaster( mDataType, mLayerWidth, mLayerHeight, mExtent, mCrs ) );
187 if ( !destProvider->isValid() )
190 destProvider->setNoDataValue( 1, mNoData );
191 destProvider->setEditable(
true );
194 const int blockPadding = 1 + radius + iterations;
196 iter.startRasterRead( mBand, mLayerWidth, mLayerHeight, mExtent );
208 std::unique_ptr<QgsRasterBlock> inputBlock;
212 const size_t maxBufferSize =
static_cast<std::size_t
>( iter.maximumTileWidth() + 2 * blockPadding ) *
static_cast< std::size_t
>( iter.maximumTileHeight() + 2 * blockPadding );
214 std::vector<Vector2D> normalBuffer( maxBufferSize );
215 std::vector<Vector2D> smoothedNormalBuffer( maxBufferSize );
216 std::vector<double> zBuffer( maxBufferSize );
217 std::vector<qint8> noDataBuffer( maxBufferSize );
219 bool isNoData =
false;
221 const bool hasReportsDuringClose = destProvider->hasReportsDuringClose();
222 const double maxProgressDuringBlockWriting = hasReportsDuringClose ? 50.0 : 100.0;
224 while ( iter.readNextRasterPart( mBand, iterCols, iterRows, inputBlock, iterLeft, iterTop, &blockExtent, &tileCols, &tileRows, &tileLeft, &tileTop ) )
228 feedback->
setProgress( maxProgressDuringBlockWriting * iter.progress( mBand, 0 ) );
229 feedback->
setProgressText( QObject::tr(
"Calculating surface normals" ) );
234 double *zBufferData = zBuffer.data();
235 qint8 *noDataBufferData = noDataBuffer.data();
236 for (
int r = 0; r < iterRows; ++r )
238 for (
int c = 0;
c < iterCols; ++
c )
240 *zBufferData++ = inputBlock->valueAndNoData( r,
c, isNoData ) * zFactor;
241 *noDataBufferData++ = isNoData ? 1 : 0;
250 auto getZ = [iterRows, iterCols, &noDataBuffer, &zBuffer](
int row,
int col,
double z ) ->
double {
251 if ( row < 0 || row >= iterRows || col < 0 || col >= iterCols )
253 std::size_t idx =
static_cast<std::size_t
>( row ) * iterCols + col;
254 if ( noDataBuffer[idx] )
259 for (
int r = 0; r < iterRows; ++r )
264 feedback->
setProgress( maxProgressDuringBlockWriting * iter.progress( mBand, r /
static_cast< double >( iterRows ) / 3.0 ) );
266 for (
int c = 0;
c < iterCols; ++
c )
268 const std::size_t idx =
static_cast<std::size_t
>( r ) * iterCols +
c;
269 if ( noDataBuffer[idx] )
271 normalBuffer[idx] = Vector2D( 0, 0 );
275 const double z = zBuffer[idx];
276 const double z1 = getZ( r - 1,
c - 1, z );
277 const double z2 = getZ( r - 1,
c, z );
278 const double z3 = getZ( r - 1,
c + 1, z );
279 const double z4 = getZ( r,
c - 1, z );
280 const double z6 = getZ( r,
c + 1, z );
281 const double z7 = getZ( r + 1,
c - 1, z );
282 const double z8 = getZ( r + 1,
c, z );
283 const double z9 = getZ( r + 1,
c + 1, z );
286 const double dx = ( ( z3 - z1 + 2 * ( z6 - z4 ) + z9 - z7 ) * oneOver8ResX );
287 const double dy = ( ( z7 - z1 + 2 * ( z8 - z2 ) + z9 - z3 ) * oneOver8ResY );
288 normalBuffer[idx] = Vector2D( -dx, dy );
295 feedback->
setProgressText( QObject::tr(
"Smoothing surface normals" ) );
296 for (
int r = 0; r < iterRows; ++r )
301 feedback->
setProgress( maxProgressDuringBlockWriting * iter.progress( mBand, 1.0 / 3.0 + r /
static_cast< double >( iterRows ) / 3.0 ) );
303 for (
int c = 0;
c < iterCols; ++
c )
305 const std::size_t idx =
static_cast<std::size_t
>( r ) * iterCols +
c;
306 if ( noDataBuffer[idx] )
309 const Vector2D ¢erNormal = normalBuffer[idx];
310 double summedWeights = 0.0;
314 for (
int kernelY = -radius; kernelY <= radius; ++kernelY )
316 for (
int kernelX = -radius; kernelX <= radius; ++kernelX )
318 const int pixelRow = r + kernelY;
319 const int pixelCol =
c + kernelX;
322 if ( pixelRow < 0 || pixelRow >= iterRows || pixelCol < 0 || pixelCol >= iterCols )
324 const std::size_t pixelIdx =
static_cast<std::size_t
>( pixelRow ) * iterCols + pixelCol;
325 if ( noDataBuffer[pixelIdx] )
328 const Vector2D &neighNormal = normalBuffer[pixelIdx];
330 const double cosAngle = centerNormal.angleBetweenCos( neighNormal );
331 if ( cosAngle > cosThreshold )
333 const double w = ( cosAngle - cosThreshold ) * ( cosAngle - cosThreshold );
335 summedX += w * neighNormal.x;
336 summedY += w * neighNormal.y;
342 summedX /= summedWeights;
343 summedY /= summedWeights;
345 smoothedNormalBuffer[idx] = Vector2D( summedX, summedY );
353 const int dx[8] = { 1, 1, 1, 0, -1, -1, -1, 0 };
354 const int dy[8] = { -1, 0, 1, 1, 1, 0, -1, -1 };
357 const double worldX[8] = { -mRasterUnitsPerPixelX, -mRasterUnitsPerPixelX, -mRasterUnitsPerPixelX, 0.0, mRasterUnitsPerPixelX, mRasterUnitsPerPixelX, mRasterUnitsPerPixelX, 0.0 };
358 const double worldY[8] = { -mRasterUnitsPerPixelY, 0.0, mRasterUnitsPerPixelY, mRasterUnitsPerPixelY, mRasterUnitsPerPixelY, 0.0, -mRasterUnitsPerPixelY, -mRasterUnitsPerPixelY };
360 for (
int iteration = 0; iteration < iterations; ++iteration )
362 feedback->
setProgressText( QObject::tr(
"Updating elevation (iteration %1/%2)" ).arg( iteration + 1 ).arg( iterations ) );
366 for (
int r = 0; r < iterRows; ++r )
368 feedback->
setProgress( maxProgressDuringBlockWriting * iter.progress( mBand, 2.0 / 3.0 + ( (
static_cast< double >( iteration ) / iterations ) + ( r /
static_cast< double >( iterRows ) ) / iterations ) / 3.0 ) );
369 for (
int c = 0;
c < iterCols; ++
c )
371 const std::size_t idx =
static_cast<std::size_t
>( r ) * iterCols +
c;
372 if ( noDataBuffer[idx] )
375 const double originalZ = inputBlock->value( r,
c );
377 const Vector2D ¢erNormal = smoothedNormalBuffer[idx];
378 double sumWeights = 0.0;
380 for (
int n = 0; n < 8; ++n )
382 const int pixelX =
c + dx[n];
383 const int pixelY = r + dy[n];
384 if ( pixelX < 0 || pixelX >= iterCols || pixelY < 0 || pixelY >= iterRows )
387 const std::size_t nIdx =
static_cast<std::size_t
>( pixelY ) * iterCols + pixelX;
388 if ( noDataBuffer[nIdx] )
391 const double pixelZ = zBuffer[nIdx];
393 const Vector2D &neighNormal = smoothedNormalBuffer[nIdx];
394 const double cosAngle = centerNormal.angleBetweenCos( neighNormal );
395 if ( cosAngle > cosThreshold )
397 const double w = ( cosAngle - cosThreshold ) * ( cosAngle - cosThreshold );
399 sumZ += -( neighNormal.x * worldX[n] + neighNormal.y * worldY[n] - pixelZ ) * w;
403 if ( sumWeights > 0.0 )
405 const double newZScaled = ( sumZ / sumWeights );
406 const double newZUnscaled = newZScaled / zFactor;
407 if ( !hasMaxElevChange || std::abs( newZUnscaled - originalZ ) <= maxElevChange )
409 zBuffer[idx] = newZScaled;
413 zBuffer[idx] = originalZ * zFactor;
418 zBuffer[idx] = originalZ * zFactor;
424 const int blockOffsetX = tileLeft - iterLeft;
425 const int blockOffsetY = tileTop - iterTop;
427 auto outputBlock = std::make_unique<QgsRasterBlock>( mDataType, tileCols, tileRows );
429 for (
int r = 0; r < tileRows; ++r )
431 for (
int c = 0;
c < tileCols; ++
c )
433 const int pixelRow = r + blockOffsetY;
434 const int pixelCol =
c + blockOffsetX;
436 const std::size_t idx =
static_cast<std::size_t
>( pixelRow ) * iterCols + pixelCol;
437 if ( noDataBuffer[idx] )
438 outputBlock->setValue( r,
c, mNoData );
440 outputBlock->setValue( r,
c, zBuffer[idx] / zFactor );
444 if ( !destProvider->writeBlock( outputBlock.get(), 1, tileLeft, tileTop ) )
446 throw QgsProcessingException( QObject::tr(
"Could not write raster block: %1" ).arg( destProvider->error().summary() ) );
449 destProvider->setEditable(
false );
451 if ( feedback && hasReportsDuringClose )
454 if ( !destProvider->closeWithProgress( scaledFeedback.get() ) )
463 outputs.insert( u
"OUTPUT"_s, outputFile );
@ Advanced
Parameter is an advanced parameter which should be hidden from users by default.
@ Double
Double/float values.
bool isCanceled() const
Tells whether the operation has been canceled already.
void setProgress(double progress)
Sets the current progress for the feedback object.
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
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.
virtual void setProgressText(const QString &text)
Sets a progress report text string.
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 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.
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.
As part of the API refactoring and improvements which landed in the Processing API was substantially reworked from the x version This was done in order to allow much of the underlying Processing framework to be ported into c
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference).