26using namespace Qt::StringLiterals;
35 Vector2D(
double x = 0,
double y = 0 )
40 double angleBetweenCos(
const Vector2D &other )
const
42 const double dot = x * other.x + y * other.y + 1.0;
43 const double magSelf = ( x * x + y * y + 1.0 );
44 const double magOther = ( other.x * other.x + other.y * other.y + 1.0 );
45 return dot / std::sqrt( magSelf * magOther );
49QString QgsRasterFeaturePreservingSmoothingAlgorithm::name()
const
51 return u
"rasterfeaturepreservingsmoothing"_s;
54QString QgsRasterFeaturePreservingSmoothingAlgorithm::displayName()
const
56 return QObject::tr(
"Feature preserving DEM smoothing" );
59QStringList QgsRasterFeaturePreservingSmoothingAlgorithm::tags()
const
61 return QObject::tr(
"smooth,filter,denoise,fpdems,blur" ).split(
',' );
64QString QgsRasterFeaturePreservingSmoothingAlgorithm::group()
const
66 return QObject::tr(
"Raster analysis" );
69QString QgsRasterFeaturePreservingSmoothingAlgorithm::groupId()
const
71 return u
"rasteranalysis"_s;
74QString QgsRasterFeaturePreservingSmoothingAlgorithm::shortHelpString()
const
77 "This algorithm applies the Feature-Preserving DEM Smoothing (FPDEMS) method, as described by Lindsay et al. (2019).\n\n"
78 "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. "
79 "This makes it superior to standard low-pass filters (e.g., mean, median, Gaussian) or resampling, which often blur distinct topographic features.\n\n"
80 "The algorithm works in three steps:\n"
81 "1. Calculating surface normal 3D vectors for each grid cell.\n"
82 "2. Smoothing the normal vector field using a filter that applies more weight to neighbors with similar surface normals (preserving edges).\n"
83 "3. Iteratively updating the elevations in the DEM to match the smoothed normal field.\n\n"
87QList<QgsAcademicReference> QgsRasterFeaturePreservingSmoothingAlgorithm::academicReferences()
const
91 lindsayReference.
setUrl( u
"https://doi.org/10.3390/rs11161926"_s );
94 hornReference.
setUrl( u
"https://doi.org/10.1109/PROC.1981.11918"_s );
95 return { lindsayReference, hornReference };
98QString QgsRasterFeaturePreservingSmoothingAlgorithm::shortDescription()
const
100 return QObject::tr(
"Smooths a DEM while preserving topographic features." );
103void QgsRasterFeaturePreservingSmoothingAlgorithm::initAlgorithm(
const QVariantMap & )
110 radiusParam->setHelp( QObject::tr(
"Radius of the filter kernel. A radius of 5 results in an 11x11 kernel." ) );
111 addParameter( radiusParam.release() );
115 thresholdParam->setHelp(
117 "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 "
118 "included, producing smoother surfaces. A range of 10-20 degrees is typically optimal."
121 addParameter( thresholdParam.release() );
124 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." ) );
125 addParameter( iterParam.release() );
129 maxDiffParam->setHelp(
130 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." )
132 addParameter( maxDiffParam.release() );
135 zFactorParam->setHelp( QObject::tr(
"Multiplication factor to convert vertical Z units to horizontal XY units." ) );
137 zFactorParam->setMetadata( { QVariantMap( { { u
"widget_wrapper"_s, QVariantMap( { { u
"decimals"_s, 12 } } ) } } ) } );
138 addParameter( zFactorParam.release() );
140 auto creationOptsParam = std::make_unique<QgsProcessingParameterString>( u
"CREATION_OPTIONS"_s, QObject::tr(
"Creation options" ), QVariant(),
false,
true );
141 creationOptsParam->setMetadata( QVariantMap( { { u
"widget_wrapper"_s, QVariantMap( { { u
"widget_type"_s, u
"rasteroptions"_s } } ) } } ) );
143 addParameter( creationOptsParam.release() );
145 auto outputLayerParam = std::make_unique<QgsProcessingParameterRasterDestination>( u
"OUTPUT"_s, QObject::tr(
"Output layer" ) );
146 addParameter( outputLayerParam.release() );
149QgsRasterFeaturePreservingSmoothingAlgorithm *QgsRasterFeaturePreservingSmoothingAlgorithm::createInstance()
const
151 return new QgsRasterFeaturePreservingSmoothingAlgorithm();
156 QgsRasterLayer *layer = parameterAsRasterLayer( parameters, u
"INPUT"_s, context );
160 mBand = parameterAsInt( parameters, u
"BAND"_s, context );
161 if ( mBand < 1 || mBand > layer->
bandCount() )
162 throw QgsProcessingException( QObject::tr(
"Invalid band number for BAND (%1): Valid values for input raster are 1 to %2" ).arg( mBand ).arg( layer->
bandCount() ) );
165 mLayerWidth = layer->
width();
166 mLayerHeight = layer->
height();
167 mExtent = layer->
extent();
178 QGS_MARK_ALGORITHM_SOURCE
180 const int radius = parameterAsInt( parameters, u
"RADIUS"_s, context );
182 const double thresholdDegrees = parameterAsDouble( parameters, u
"THRESHOLD"_s, context );
183 const double cosThreshold = std::cos( thresholdDegrees * M_PI / 180.0 );
185 const int iterations = parameterAsInt( parameters, u
"ITERATIONS"_s, context );
186 const bool hasMaxElevChange = parameters.value( u
"MAX_ELEVATION_CHANGE"_s ).isValid();
187 const double maxElevChange = hasMaxElevChange ? parameterAsDouble( parameters, u
"MAX_ELEVATION_CHANGE"_s, context ) : 0;
188 const double zFactor = parameterAsDouble( parameters, u
"Z_FACTOR"_s, context );
190 const double oneOver8ResX = 1 / ( 8.0 * mRasterUnitsPerPixelX );
191 const double oneOver8ResY = 1 / ( 8.0 * mRasterUnitsPerPixelY );
193 const QString creationOptions = parameterAsString( parameters, u
"CREATION_OPTIONS"_s, context ).trimmed();
194 const QString outputFile = parameterAsOutputLayer( parameters, u
"OUTPUT"_s, context );
195 const QString outputFormat = parameterAsOutputRasterFormat( parameters, u
"OUTPUT"_s, context );
197 auto outputWriter = std::make_unique<QgsRasterFileWriter>( outputFile );
198 outputWriter->setOutputProviderKey( u
"gdal"_s );
199 if ( !creationOptions.isEmpty() )
201 outputWriter->setCreationOptions( creationOptions.split(
'|' ) );
203 outputWriter->setOutputFormat( outputFormat );
205 std::unique_ptr<QgsRasterDataProvider> destProvider( outputWriter->createOneBandRaster( mDataType, mLayerWidth, mLayerHeight, mExtent, mCrs ) );
208 if ( !destProvider->isValid() )
211 destProvider->setNoDataValue( 1, mNoData );
212 destProvider->setEditable(
true );
215 const int blockPadding = 1 + radius + iterations;
217 iter.startRasterRead( mBand, mLayerWidth, mLayerHeight, mExtent );
229 std::unique_ptr<QgsRasterBlock> inputBlock;
233 const size_t maxBufferSize =
static_cast<std::size_t
>( iter.maximumTileWidth() + 2 * blockPadding ) *
static_cast< std::size_t
>( iter.maximumTileHeight() + 2 * blockPadding );
235 std::vector<Vector2D> normalBuffer( maxBufferSize );
236 std::vector<Vector2D> smoothedNormalBuffer( maxBufferSize );
237 std::vector<double> zBuffer( maxBufferSize );
238 std::vector<qint8> noDataBuffer( maxBufferSize );
240 bool isNoData =
false;
242 const bool hasReportsDuringClose = destProvider->hasReportsDuringClose();
243 const double maxProgressDuringBlockWriting = hasReportsDuringClose ? 50.0 : 100.0;
245 while ( iter.readNextRasterPart( mBand, iterCols, iterRows, inputBlock, iterLeft, iterTop, &blockExtent, &tileCols, &tileRows, &tileLeft, &tileTop ) )
249 feedback->
setProgress( maxProgressDuringBlockWriting * iter.progress( mBand, 0 ) );
250 feedback->
setProgressText( QObject::tr(
"Calculating surface normals" ) );
255 double *zBufferData = zBuffer.data();
256 qint8 *noDataBufferData = noDataBuffer.data();
257 for (
int r = 0; r < iterRows; ++r )
259 for (
int c = 0;
c < iterCols; ++
c )
261 *zBufferData++ = inputBlock->valueAndNoData( r,
c, isNoData ) * zFactor;
262 *noDataBufferData++ = isNoData ? 1 : 0;
271 auto getZ = [iterRows, iterCols, &noDataBuffer, &zBuffer](
int row,
int col,
double z ) ->
double {
272 if ( row < 0 || row >= iterRows || col < 0 || col >= iterCols )
274 std::size_t idx =
static_cast<std::size_t
>( row ) * iterCols + col;
275 if ( noDataBuffer[idx] )
280 for (
int r = 0; r < iterRows; ++r )
285 feedback->
setProgress( maxProgressDuringBlockWriting * iter.progress( mBand, r /
static_cast< double >( iterRows ) / 3.0 ) );
287 for (
int c = 0;
c < iterCols; ++
c )
289 const std::size_t idx =
static_cast<std::size_t
>( r ) * iterCols +
c;
290 if ( noDataBuffer[idx] )
292 normalBuffer[idx] = Vector2D( 0, 0 );
296 const double z = zBuffer[idx];
297 const double z1 = getZ( r - 1,
c - 1, z );
298 const double z2 = getZ( r - 1,
c, z );
299 const double z3 = getZ( r - 1,
c + 1, z );
300 const double z4 = getZ( r,
c - 1, z );
301 const double z6 = getZ( r,
c + 1, z );
302 const double z7 = getZ( r + 1,
c - 1, z );
303 const double z8 = getZ( r + 1,
c, z );
304 const double z9 = getZ( r + 1,
c + 1, z );
307 const double dx = ( ( z3 - z1 + 2 * ( z6 - z4 ) + z9 - z7 ) * oneOver8ResX );
308 const double dy = ( ( z7 - z1 + 2 * ( z8 - z2 ) + z9 - z3 ) * oneOver8ResY );
309 normalBuffer[idx] = Vector2D( -dx, dy );
316 feedback->
setProgressText( QObject::tr(
"Smoothing surface normals" ) );
317 for (
int r = 0; r < iterRows; ++r )
322 feedback->
setProgress( maxProgressDuringBlockWriting * iter.progress( mBand, 1.0 / 3.0 + r /
static_cast< double >( iterRows ) / 3.0 ) );
324 for (
int c = 0;
c < iterCols; ++
c )
326 const std::size_t idx =
static_cast<std::size_t
>( r ) * iterCols +
c;
327 if ( noDataBuffer[idx] )
330 const Vector2D ¢erNormal = normalBuffer[idx];
331 double summedWeights = 0.0;
335 for (
int kernelY = -radius; kernelY <= radius; ++kernelY )
337 for (
int kernelX = -radius; kernelX <= radius; ++kernelX )
339 const int pixelRow = r + kernelY;
340 const int pixelCol =
c + kernelX;
343 if ( pixelRow < 0 || pixelRow >= iterRows || pixelCol < 0 || pixelCol >= iterCols )
345 const std::size_t pixelIdx =
static_cast<std::size_t
>( pixelRow ) * iterCols + pixelCol;
346 if ( noDataBuffer[pixelIdx] )
349 const Vector2D &neighNormal = normalBuffer[pixelIdx];
351 const double cosAngle = centerNormal.angleBetweenCos( neighNormal );
352 if ( cosAngle > cosThreshold )
354 const double w = ( cosAngle - cosThreshold ) * ( cosAngle - cosThreshold );
356 summedX += w * neighNormal.x;
357 summedY += w * neighNormal.y;
363 summedX /= summedWeights;
364 summedY /= summedWeights;
366 smoothedNormalBuffer[idx] = Vector2D( summedX, summedY );
374 const int dx[8] = { 1, 1, 1, 0, -1, -1, -1, 0 };
375 const int dy[8] = { -1, 0, 1, 1, 1, 0, -1, -1 };
378 const double worldX[8] = { -mRasterUnitsPerPixelX, -mRasterUnitsPerPixelX, -mRasterUnitsPerPixelX, 0.0, mRasterUnitsPerPixelX, mRasterUnitsPerPixelX, mRasterUnitsPerPixelX, 0.0 };
379 const double worldY[8] = { -mRasterUnitsPerPixelY, 0.0, mRasterUnitsPerPixelY, mRasterUnitsPerPixelY, mRasterUnitsPerPixelY, 0.0, -mRasterUnitsPerPixelY, -mRasterUnitsPerPixelY };
381 for (
int iteration = 0; iteration < iterations; ++iteration )
383 feedback->
setProgressText( QObject::tr(
"Updating elevation (iteration %1/%2)" ).arg( iteration + 1 ).arg( iterations ) );
387 for (
int r = 0; r < iterRows; ++r )
390 maxProgressDuringBlockWriting * iter.progress( mBand, 2.0 / 3.0 + ( (
static_cast< double >( iteration ) / iterations ) + ( r /
static_cast< double >( iterRows ) ) / iterations ) / 3.0 )
392 for (
int c = 0;
c < iterCols; ++
c )
394 const std::size_t idx =
static_cast<std::size_t
>( r ) * iterCols +
c;
395 if ( noDataBuffer[idx] )
398 const double originalZ = inputBlock->value( r,
c );
400 const Vector2D ¢erNormal = smoothedNormalBuffer[idx];
401 double sumWeights = 0.0;
403 for (
int n = 0; n < 8; ++n )
405 const int pixelX =
c + dx[n];
406 const int pixelY = r + dy[n];
407 if ( pixelX < 0 || pixelX >= iterCols || pixelY < 0 || pixelY >= iterRows )
410 const std::size_t nIdx =
static_cast<std::size_t
>( pixelY ) * iterCols + pixelX;
411 if ( noDataBuffer[nIdx] )
414 const double pixelZ = zBuffer[nIdx];
416 const Vector2D &neighNormal = smoothedNormalBuffer[nIdx];
417 const double cosAngle = centerNormal.angleBetweenCos( neighNormal );
418 if ( cosAngle > cosThreshold )
420 const double w = ( cosAngle - cosThreshold ) * ( cosAngle - cosThreshold );
422 sumZ += -( neighNormal.x * worldX[n] + neighNormal.y * worldY[n] - pixelZ ) * w;
426 if ( sumWeights > 0.0 )
428 const double newZScaled = ( sumZ / sumWeights );
429 const double newZUnscaled = newZScaled / zFactor;
430 if ( !hasMaxElevChange || std::abs( newZUnscaled - originalZ ) <= maxElevChange )
432 zBuffer[idx] = newZScaled;
436 zBuffer[idx] = originalZ * zFactor;
441 zBuffer[idx] = originalZ * zFactor;
447 const int blockOffsetX = tileLeft - iterLeft;
448 const int blockOffsetY = tileTop - iterTop;
450 auto outputBlock = std::make_unique<QgsRasterBlock>( mDataType, tileCols, tileRows );
452 for (
int r = 0; r < tileRows; ++r )
454 for (
int c = 0;
c < tileCols; ++
c )
456 const int pixelRow = r + blockOffsetY;
457 const int pixelCol =
c + blockOffsetX;
459 const std::size_t idx =
static_cast<std::size_t
>( pixelRow ) * iterCols + pixelCol;
460 if ( noDataBuffer[idx] )
461 outputBlock->setValue( r,
c, mNoData );
463 outputBlock->setValue( r,
c, zBuffer[idx] / zFactor );
467 if ( !destProvider->writeBlock( outputBlock.get(), 1, tileLeft, tileTop ) )
469 throw QgsProcessingException( QObject::tr(
"Could not write raster block: %1" ).arg( destProvider->error().summary() ) );
472 destProvider->setEditable(
false );
474 if ( feedback && hasReportsDuringClose )
477 if ( !destProvider->closeWithProgress( scaledFeedback.get() ) )
486 outputs.insert( u
"OUTPUT"_s, outputFile );
@ Advanced
Parameter is an advanced parameter which should be hidden from users by default.
@ Double
Double/float values.
Encapsulates an academic reference and formats it according to style guidelines.
void setUrl(const QString &url)
Sets the url.
static QgsAcademicReference createJournalArticle(const QStringList &authors, int year, const QString &title, const QString &journal, const QString &volume=QString(), const QString &issue=QString(), const QString &pages=QString())
Creates a journal article reference.
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 Q_INVOKABLE 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).