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
75 "This algorithm applies the Feature-Preserving DEM Smoothing (FPDEMS) method, as described by Lindsay et al. (2019).\n\n"
76 "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. "
77 "This makes it superior to standard low-pass filters (e.g., mean, median, Gaussian) or resampling, which often blur distinct topographic features.\n\n"
78 "The algorithm works in three steps:\n"
79 "1. Calculating surface normal 3D vectors for each grid cell.\n"
80 "2. Smoothing the normal vector field using a filter that applies more weight to neighbors with similar surface normals (preserving edges).\n"
81 "3. Iteratively updating the elevations in the DEM to match the smoothed normal field.\n\n"
82 "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"
86QString QgsRasterFeaturePreservingSmoothingAlgorithm::shortDescription()
const
88 return QObject::tr(
"Smooths a DEM while preserving topographic features." );
91void QgsRasterFeaturePreservingSmoothingAlgorithm::initAlgorithm(
const QVariantMap & )
98 radiusParam->setHelp( QObject::tr(
"Radius of the filter kernel. A radius of 5 results in an 11x11 kernel." ) );
99 addParameter( radiusParam.release() );
103 thresholdParam->setHelp(
105 "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 "
106 "included, producing smoother surfaces. A range of 10-20 degrees is typically optimal."
109 addParameter( thresholdParam.release() );
112 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." ) );
113 addParameter( iterParam.release() );
117 maxDiffParam->setHelp(
118 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." )
120 addParameter( maxDiffParam.release() );
123 zFactorParam->setHelp( QObject::tr(
"Multiplication factor to convert vertical Z units to horizontal XY units." ) );
125 zFactorParam->setMetadata( { QVariantMap( { { u
"widget_wrapper"_s, QVariantMap( { { u
"decimals"_s, 12 } } ) } } ) } );
126 addParameter( zFactorParam.release() );
128 auto creationOptsParam = std::make_unique<QgsProcessingParameterString>( u
"CREATION_OPTIONS"_s, QObject::tr(
"Creation options" ), QVariant(),
false,
true );
129 creationOptsParam->setMetadata( QVariantMap( { { u
"widget_wrapper"_s, QVariantMap( { { u
"widget_type"_s, u
"rasteroptions"_s } } ) } } ) );
131 addParameter( creationOptsParam.release() );
133 auto outputLayerParam = std::make_unique<QgsProcessingParameterRasterDestination>( u
"OUTPUT"_s, QObject::tr(
"Output layer" ) );
134 addParameter( outputLayerParam.release() );
137QgsRasterFeaturePreservingSmoothingAlgorithm *QgsRasterFeaturePreservingSmoothingAlgorithm::createInstance()
const
139 return new QgsRasterFeaturePreservingSmoothingAlgorithm();
144 QgsRasterLayer *layer = parameterAsRasterLayer( parameters, u
"INPUT"_s, context );
148 mBand = parameterAsInt( parameters, u
"BAND"_s, context );
149 if ( mBand < 1 || mBand > layer->
bandCount() )
150 throw QgsProcessingException( QObject::tr(
"Invalid band number for BAND (%1): Valid values for input raster are 1 to %2" ).arg( mBand ).arg( layer->
bandCount() ) );
153 mLayerWidth = layer->
width();
154 mLayerHeight = layer->
height();
155 mExtent = layer->
extent();
166 const int radius = parameterAsInt( parameters, u
"RADIUS"_s, context );
168 const double thresholdDegrees = parameterAsDouble( parameters, u
"THRESHOLD"_s, context );
169 const double cosThreshold = std::cos( thresholdDegrees * M_PI / 180.0 );
171 const int iterations = parameterAsInt( parameters, u
"ITERATIONS"_s, context );
172 const bool hasMaxElevChange = parameters.value( u
"MAX_ELEVATION_CHANGE"_s ).isValid();
173 const double maxElevChange = hasMaxElevChange ? parameterAsDouble( parameters, u
"MAX_ELEVATION_CHANGE"_s, context ) : 0;
174 const double zFactor = parameterAsDouble( parameters, u
"Z_FACTOR"_s, context );
176 const double oneOver8ResX = 1 / ( 8.0 * mRasterUnitsPerPixelX );
177 const double oneOver8ResY = 1 / ( 8.0 * mRasterUnitsPerPixelY );
179 const QString creationOptions = parameterAsString( parameters, u
"CREATION_OPTIONS"_s, context ).trimmed();
180 const QString outputFile = parameterAsOutputLayer( parameters, u
"OUTPUT"_s, context );
181 const QString outputFormat = parameterAsOutputRasterFormat( parameters, u
"OUTPUT"_s, context );
183 auto outputWriter = std::make_unique<QgsRasterFileWriter>( outputFile );
184 outputWriter->setOutputProviderKey( u
"gdal"_s );
185 if ( !creationOptions.isEmpty() )
187 outputWriter->setCreationOptions( creationOptions.split(
'|' ) );
189 outputWriter->setOutputFormat( outputFormat );
191 std::unique_ptr<QgsRasterDataProvider> destProvider( outputWriter->createOneBandRaster( mDataType, mLayerWidth, mLayerHeight, mExtent, mCrs ) );
194 if ( !destProvider->isValid() )
197 destProvider->setNoDataValue( 1, mNoData );
198 destProvider->setEditable(
true );
201 const int blockPadding = 1 + radius + iterations;
203 iter.startRasterRead( mBand, mLayerWidth, mLayerHeight, mExtent );
215 std::unique_ptr<QgsRasterBlock> inputBlock;
219 const size_t maxBufferSize =
static_cast<std::size_t
>( iter.maximumTileWidth() + 2 * blockPadding ) *
static_cast< std::size_t
>( iter.maximumTileHeight() + 2 * blockPadding );
221 std::vector<Vector2D> normalBuffer( maxBufferSize );
222 std::vector<Vector2D> smoothedNormalBuffer( maxBufferSize );
223 std::vector<double> zBuffer( maxBufferSize );
224 std::vector<qint8> noDataBuffer( maxBufferSize );
226 bool isNoData =
false;
228 const bool hasReportsDuringClose = destProvider->hasReportsDuringClose();
229 const double maxProgressDuringBlockWriting = hasReportsDuringClose ? 50.0 : 100.0;
231 while ( iter.readNextRasterPart( mBand, iterCols, iterRows, inputBlock, iterLeft, iterTop, &blockExtent, &tileCols, &tileRows, &tileLeft, &tileTop ) )
235 feedback->
setProgress( maxProgressDuringBlockWriting * iter.progress( mBand, 0 ) );
236 feedback->
setProgressText( QObject::tr(
"Calculating surface normals" ) );
241 double *zBufferData = zBuffer.data();
242 qint8 *noDataBufferData = noDataBuffer.data();
243 for (
int r = 0; r < iterRows; ++r )
245 for (
int c = 0;
c < iterCols; ++
c )
247 *zBufferData++ = inputBlock->valueAndNoData( r,
c, isNoData ) * zFactor;
248 *noDataBufferData++ = isNoData ? 1 : 0;
257 auto getZ = [iterRows, iterCols, &noDataBuffer, &zBuffer](
int row,
int col,
double z ) ->
double {
258 if ( row < 0 || row >= iterRows || col < 0 || col >= iterCols )
260 std::size_t idx =
static_cast<std::size_t
>( row ) * iterCols + col;
261 if ( noDataBuffer[idx] )
266 for (
int r = 0; r < iterRows; ++r )
271 feedback->
setProgress( maxProgressDuringBlockWriting * iter.progress( mBand, r /
static_cast< double >( iterRows ) / 3.0 ) );
273 for (
int c = 0;
c < iterCols; ++
c )
275 const std::size_t idx =
static_cast<std::size_t
>( r ) * iterCols +
c;
276 if ( noDataBuffer[idx] )
278 normalBuffer[idx] = Vector2D( 0, 0 );
282 const double z = zBuffer[idx];
283 const double z1 = getZ( r - 1,
c - 1, z );
284 const double z2 = getZ( r - 1,
c, z );
285 const double z3 = getZ( r - 1,
c + 1, z );
286 const double z4 = getZ( r,
c - 1, z );
287 const double z6 = getZ( r,
c + 1, z );
288 const double z7 = getZ( r + 1,
c - 1, z );
289 const double z8 = getZ( r + 1,
c, z );
290 const double z9 = getZ( r + 1,
c + 1, z );
293 const double dx = ( ( z3 - z1 + 2 * ( z6 - z4 ) + z9 - z7 ) * oneOver8ResX );
294 const double dy = ( ( z7 - z1 + 2 * ( z8 - z2 ) + z9 - z3 ) * oneOver8ResY );
295 normalBuffer[idx] = Vector2D( -dx, dy );
302 feedback->
setProgressText( QObject::tr(
"Smoothing surface normals" ) );
303 for (
int r = 0; r < iterRows; ++r )
308 feedback->
setProgress( maxProgressDuringBlockWriting * iter.progress( mBand, 1.0 / 3.0 + r /
static_cast< double >( iterRows ) / 3.0 ) );
310 for (
int c = 0;
c < iterCols; ++
c )
312 const std::size_t idx =
static_cast<std::size_t
>( r ) * iterCols +
c;
313 if ( noDataBuffer[idx] )
316 const Vector2D ¢erNormal = normalBuffer[idx];
317 double summedWeights = 0.0;
321 for (
int kernelY = -radius; kernelY <= radius; ++kernelY )
323 for (
int kernelX = -radius; kernelX <= radius; ++kernelX )
325 const int pixelRow = r + kernelY;
326 const int pixelCol =
c + kernelX;
329 if ( pixelRow < 0 || pixelRow >= iterRows || pixelCol < 0 || pixelCol >= iterCols )
331 const std::size_t pixelIdx =
static_cast<std::size_t
>( pixelRow ) * iterCols + pixelCol;
332 if ( noDataBuffer[pixelIdx] )
335 const Vector2D &neighNormal = normalBuffer[pixelIdx];
337 const double cosAngle = centerNormal.angleBetweenCos( neighNormal );
338 if ( cosAngle > cosThreshold )
340 const double w = ( cosAngle - cosThreshold ) * ( cosAngle - cosThreshold );
342 summedX += w * neighNormal.x;
343 summedY += w * neighNormal.y;
349 summedX /= summedWeights;
350 summedY /= summedWeights;
352 smoothedNormalBuffer[idx] = Vector2D( summedX, summedY );
360 const int dx[8] = { 1, 1, 1, 0, -1, -1, -1, 0 };
361 const int dy[8] = { -1, 0, 1, 1, 1, 0, -1, -1 };
364 const double worldX[8] = { -mRasterUnitsPerPixelX, -mRasterUnitsPerPixelX, -mRasterUnitsPerPixelX, 0.0, mRasterUnitsPerPixelX, mRasterUnitsPerPixelX, mRasterUnitsPerPixelX, 0.0 };
365 const double worldY[8] = { -mRasterUnitsPerPixelY, 0.0, mRasterUnitsPerPixelY, mRasterUnitsPerPixelY, mRasterUnitsPerPixelY, 0.0, -mRasterUnitsPerPixelY, -mRasterUnitsPerPixelY };
367 for (
int iteration = 0; iteration < iterations; ++iteration )
369 feedback->
setProgressText( QObject::tr(
"Updating elevation (iteration %1/%2)" ).arg( iteration + 1 ).arg( iterations ) );
373 for (
int r = 0; r < iterRows; ++r )
376 maxProgressDuringBlockWriting * iter.progress( mBand, 2.0 / 3.0 + ( (
static_cast< double >( iteration ) / iterations ) + ( r /
static_cast< double >( iterRows ) ) / iterations ) / 3.0 )
378 for (
int c = 0;
c < iterCols; ++
c )
380 const std::size_t idx =
static_cast<std::size_t
>( r ) * iterCols +
c;
381 if ( noDataBuffer[idx] )
384 const double originalZ = inputBlock->value( r,
c );
386 const Vector2D ¢erNormal = smoothedNormalBuffer[idx];
387 double sumWeights = 0.0;
389 for (
int n = 0; n < 8; ++n )
391 const int pixelX =
c + dx[n];
392 const int pixelY = r + dy[n];
393 if ( pixelX < 0 || pixelX >= iterCols || pixelY < 0 || pixelY >= iterRows )
396 const std::size_t nIdx =
static_cast<std::size_t
>( pixelY ) * iterCols + pixelX;
397 if ( noDataBuffer[nIdx] )
400 const double pixelZ = zBuffer[nIdx];
402 const Vector2D &neighNormal = smoothedNormalBuffer[nIdx];
403 const double cosAngle = centerNormal.angleBetweenCos( neighNormal );
404 if ( cosAngle > cosThreshold )
406 const double w = ( cosAngle - cosThreshold ) * ( cosAngle - cosThreshold );
408 sumZ += -( neighNormal.x * worldX[n] + neighNormal.y * worldY[n] - pixelZ ) * w;
412 if ( sumWeights > 0.0 )
414 const double newZScaled = ( sumZ / sumWeights );
415 const double newZUnscaled = newZScaled / zFactor;
416 if ( !hasMaxElevChange || std::abs( newZUnscaled - originalZ ) <= maxElevChange )
418 zBuffer[idx] = newZScaled;
422 zBuffer[idx] = originalZ * zFactor;
427 zBuffer[idx] = originalZ * zFactor;
433 const int blockOffsetX = tileLeft - iterLeft;
434 const int blockOffsetY = tileTop - iterTop;
436 auto outputBlock = std::make_unique<QgsRasterBlock>( mDataType, tileCols, tileRows );
438 for (
int r = 0; r < tileRows; ++r )
440 for (
int c = 0;
c < tileCols; ++
c )
442 const int pixelRow = r + blockOffsetY;
443 const int pixelCol =
c + blockOffsetX;
445 const std::size_t idx =
static_cast<std::size_t
>( pixelRow ) * iterCols + pixelCol;
446 if ( noDataBuffer[idx] )
447 outputBlock->setValue( r,
c, mNoData );
449 outputBlock->setValue( r,
c, zBuffer[idx] / zFactor );
453 if ( !destProvider->writeBlock( outputBlock.get(), 1, tileLeft, tileTop ) )
455 throw QgsProcessingException( QObject::tr(
"Could not write raster block: %1" ).arg( destProvider->error().summary() ) );
458 destProvider->setEditable(
false );
460 if ( feedback && hasReportsDuringClose )
463 if ( !destProvider->closeWithProgress( scaledFeedback.get() ) )
472 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).