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