QGIS API Documentation 4.3.0-Master (2b7e6c9893e)
Loading...
Searching...
No Matches
qgsalgorithmtpsinterpolation.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsalgorithmtpsinterpolation.cpp
3 -----------------------------------
4 begin : August 2026
5 copyright : (C) 2026 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 <numeric>
21
23#include "qgsapplication.h"
24#include "qgsmatrixsolver.h"
27#include "qgsprocessingutils.h"
28#include "qgsrasterfilewriter.h"
29#include "qgsrasteriterator.h"
31#include "qgsvariantutils.h"
32
33#include <QString>
34#include <QtConcurrentMap>
35
36using namespace Qt::StringLiterals;
37
39
40//
41// QgsThinPlateSplineAlgorithmBase
42//
43
44QIcon QgsThinPlateSplineAlgorithmBase::icon() const
45{
46 return QgsApplication::getThemeIcon( u"/algorithms/mAlgorithmInterpolation.svg"_s );
47}
48
49QString QgsThinPlateSplineAlgorithmBase::svgIconPath() const
50{
51 return QgsApplication::iconPath( u"/algorithms/mAlgorithmInterpolation.svg"_s );
52}
53
54QString QgsThinPlateSplineAlgorithmBase::group() const
55{
56 return QObject::tr( "Interpolation" );
57}
58
59QString QgsThinPlateSplineAlgorithmBase::groupId() const
60{
61 return u"interpolation"_s;
62}
63
64QList<QgsAcademicReference> QgsThinPlateSplineAlgorithmBase::academicReferences() const
65{
67 { u"Donato, G."_s, u"Belongie, S."_s },
68 2002,
69 u"Approximation Methods for Thin Plate Spline Mappings and Principal Warps"_s,
70 u"In Heyden, A., Sparr, G., Nielsen, M., Johansen, P. (Eds.): Computer Vision - ECCV 2002: 7th European Conference on Computer Vision, Copenhagen, Denmark, May 28-31, 2002, Proceedings, Part III, Lecture Notes in Computer Science."_s,
71 u"Springer-Verlag Heidelberg"_s,
72 u"21-31"_s
73 );
74
75 const QgsAcademicReference elonenReference
76 = QgsAcademicReference::createWebPage( { u"Elonen, J."_s }, 2005, u"Thin Plate Spline editor - an example program in C++"_s, u"http://elonen.iki.fi/code/tpsdemo/index.html"_s );
77 return { donatoReference, elonenReference };
78}
79
80QList<QgsProcessingAlgorithm::ExternalLink> QgsThinPlateSplineAlgorithmBase::externalLinks() const
81{
82 return {
83 QgsProcessingAlgorithm::ExternalLink { QObject::tr( "SAGA tool source code" ), u"https://sourceforge.net/p/saga-gis/code/ci/33d1062b7120c696c9dd258378c48d86dc33560c/tree/saga-gis/src/tools/grid/grid_spline/Gridding_Spline_TPS_Local.cpp"_s }
84 };
85}
86
87void QgsThinPlateSplineAlgorithmBase::addCommonParameters()
88{
89 auto inputParam = std::make_unique<QgsProcessingParameterFeatureSource>( u"INPUT"_s, QObject::tr( "Point layer" ), QList<int> { static_cast< int >( Qgis::ProcessingSourceType::VectorPoint ) } );
90 inputParam->setHelp( QObject::tr( "Vector point layer containing scattered control points with 3D coordinate or attribute values." ) );
91 addParameter( inputParam.release() );
92
93 auto fieldParam = std::make_unique<QgsProcessingParameterField>( u"FIELD"_s, QObject::tr( "Z Field" ), QVariant(), u"INPUT"_s, Qgis::ProcessingFieldParameterDataType::Numeric );
94 fieldParam->setHelp( QObject::tr( "Numeric attribute field containing the values (elevation/Z) to interpolate." ) );
95 addParameter( fieldParam.release() );
96
97 auto regularizationParam = std::make_unique<QgsProcessingParameterNumber>( u"REGULARIZATION"_s, QObject::tr( "Regularization" ), Qgis::ProcessingNumberParameterType::Double, 0.0001, false, 0.0 );
98 regularizationParam->setHelp(
99 QObject::tr(
100 "Regularization parameter (lambda), where a value of 0 produces an exact spline interpolation passing precisely "
101 "through all control points. Values > 0 introduce smoothing/tension to reduce noise and flatten high-frequency variations."
102 )
103 );
104 addParameter( regularizationParam.release() );
105}
106
107void QgsThinPlateSplineAlgorithmBase::addOutputParameters()
108{
109 auto extentParam = std::make_unique<QgsProcessingParameterExtent>( u"EXTENT"_s, QObject::tr( "Extent" ), QVariant(), false );
110 extentParam->setHelp( QObject::tr( "Bounding box defining the extent of the output raster grid." ) );
111 addParameter( extentParam.release() );
112
113 auto pixelSizeParam = std::make_unique<QgsProcessingParameterInterpolationPixelSize>( u"PIXEL_SIZE"_s, QObject::tr( "Output raster size" ), u"INTERPOLATION_DATA"_s, u"EXTENT"_s, 0.1 );
114 pixelSizeParam->setHelp( QObject::tr( "Pixel size in layer units used to calculate output grid dimensions." ) );
115 addParameter( pixelSizeParam.release() );
116
117 auto outputNodataParam = std::make_unique<QgsProcessingParameterNumber>( u"NODATA"_s, QObject::tr( "Output NoData value" ), Qgis::ProcessingNumberParameterType::Double, -9999.0 );
118 outputNodataParam->setHelp( QObject::tr( "The NODATA value to use in the output raster." ) );
119 outputNodataParam->setFlags( outputNodataParam->flags() | Qgis::ProcessingParameterFlag::Advanced );
120 addParameter( outputNodataParam.release() );
121
122 auto creationOptsParam = std::make_unique<QgsProcessingParameterString>( u"CREATION_OPTIONS"_s, QObject::tr( "Creation options" ), QVariant(), false, true );
123 creationOptsParam->setHelp( QObject::tr( "The raster creation options for the output raster. These options control things like colorimetry, compression, etc." ) );
124 creationOptsParam->setMetadata( QVariantMap( { { u"widget_wrapper"_s, QVariantMap( { { u"widget_type"_s, u"rasteroptions"_s } } ) } } ) );
125 creationOptsParam->setFlags( creationOptsParam->flags() | Qgis::ProcessingParameterFlag::Advanced );
126 addParameter( creationOptsParam.release() );
127
128 auto outputParam = std::make_unique<QgsProcessingParameterRasterDestination>( u"OUTPUT"_s, QObject::tr( "Interpolated" ) );
129 addParameter( outputParam.release() );
130}
131
132void QgsThinPlateSplineAlgorithmBase::processBase( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback * )
133{
135 {
136 throw QgsProcessingException( QObject::tr( "This algorithm requires a QGIS build with GSL support enabled." ) );
137 }
138
139 mSource.reset( parameterAsSource( parameters, u"INPUT"_s, context ) );
140 if ( !mSource )
141 throw QgsProcessingException( invalidSourceError( parameters, u"INPUT"_s ) );
142
143 mFieldName = parameterAsString( parameters, u"FIELD"_s, context );
144 mRegularization = parameterAsDouble( parameters, u"REGULARIZATION"_s, context );
145
146 mExtent = parameterAsExtent( parameters, u"EXTENT"_s, context, mSource->sourceCrs() );
147
148 mPixelSize = parameterAsDouble( parameters, u"PIXEL_SIZE"_s, context );
149 mOutputPath = parameterAsOutputLayer( parameters, u"OUTPUT"_s, context );
150 mCreationOptions = parameterAsString( parameters, u"CREATION_OPTIONS"_s, context ).trimmed();
151 mNoDataValue = parameterAsDouble( parameters, u"NODATA"_s, context );
152
153 mFieldIndex = mSource->fields().lookupField( mFieldName );
154 if ( mFieldIndex < 0 )
155 {
156 throw QgsProcessingException( QObject::tr( "Attribute field '%1' was not found in input layer." ).arg( mFieldName ) );
157 }
158}
159
160
161namespace
162{
163
164 struct Neighbor
165 {
166 double x;
167 double y;
168 double z;
169 double distSq;
170 bool operator<( const Neighbor &other ) const { return distSq < other.distSq; }
171 };
172
173 struct CellStats
174 {
175 qsizetype totalCells = 0;
176 qsizetype validCells = 0;
177 qsizetype unsolvableCells = 0;
178 std::size_t minNeighbors = std::numeric_limits<std::size_t>::max();
179 std::size_t maxNeighbors = 0;
180 unsigned long long sumNeighbors = 0;
181 double minDistance = std::numeric_limits<double>::max();
182 double maxDistance = 0.0;
183 double sumDistance = 0.0;
184 unsigned long long countDistances = 0;
185
186 void merge( const CellStats &other )
187 {
188 totalCells += other.totalCells;
189 validCells += other.validCells;
190 unsolvableCells += other.unsolvableCells;
191 if ( other.minNeighbors != std::numeric_limits<std::size_t>::max() )
192 {
193 minNeighbors = std::min( minNeighbors, other.minNeighbors );
194 maxNeighbors = std::max( maxNeighbors, other.maxNeighbors );
195 }
196 sumNeighbors += other.sumNeighbors;
197 if ( other.minDistance != std::numeric_limits<double>::max() )
198 {
199 minDistance = std::min( minDistance, other.minDistance );
200 maxDistance = std::max( maxDistance, other.maxDistance );
201 }
202 sumDistance += other.sumDistance;
203 countDistances += other.countDistances;
204 }
205 };
206
207 struct RowResult
208 {
209 int r = 0;
210 std::vector<double> values;
211 CellStats stats;
212 };
213
214 typedef QHash<QgsFeatureId, double> FeatureZValueHash;
215} //namespace
216
217
218//
219// QgsLocalThinPlateSplineAlgorithm
220//
221
222QgsLocalThinPlateSplineAlgorithm::QgsLocalThinPlateSplineAlgorithm() = default;
223
224QString QgsLocalThinPlateSplineAlgorithm::name() const
225{
226 return u"localtpsinterpolation"_s;
227}
228
229QString QgsLocalThinPlateSplineAlgorithm::displayName() const
230{
231 return QObject::tr( "Thin Plate Spline interpolation (local)" );
232}
233
234QStringList QgsLocalThinPlateSplineAlgorithm::tags() const
235{
236 return QObject::tr( "tps,thin,plate,spline,interpolation,surface" ).split( ',' );
237}
238
239QString QgsLocalThinPlateSplineAlgorithm::shortDescription() const
240{
241 return QObject::tr( "Generates a Thin Plate Spline interpolation from scattered vector points." );
242}
243
244QString QgsLocalThinPlateSplineAlgorithm::shortHelpString() const
245{
246 return QObject::tr(
247 "This algorithm creates a 'Thin Plate Spline' (TPS) surface for each grid point based on scattered data points "
248 "within a specified local search distance. The number of points evaluated per cell can be constrained "
249 "to a maximum number of closest neighbors.\n\n"
250 "Thin Plate Splines minimize the integral of the squared second derivatives, creating a smooth surface "
251 "resembling a bent thin metal plate. Regularisation allows softening the exact fitting constraint to smooth out noise.\n\n"
252 "This algorithm is a port of the SAGA 'Thin Plate Spline' tool."
253 );
254}
255
256QgsLocalThinPlateSplineAlgorithm *QgsLocalThinPlateSplineAlgorithm::createInstance() const
257{
258 return new QgsLocalThinPlateSplineAlgorithm();
259}
260
261void QgsLocalThinPlateSplineAlgorithm::initAlgorithm( const QVariantMap & )
262{
263 addCommonParameters();
264
265 auto radiusParam = std::make_unique<QgsProcessingParameterDistance>( u"SEARCH_RADIUS"_s, QObject::tr( "Maximum Search Distance" ), 1000.0, u"INPUT"_s, false, 0.0 );
266 radiusParam->setHelp( QObject::tr( "Local maximum search radius. Points farther than this distance from a grid cell center are ignored." ) );
267 addParameter( radiusParam.release() );
268
269 auto maxPtsParam = std::make_unique<QgsProcessingParameterNumber>( u"SEARCH_POINTS_MAX"_s, QObject::tr( "Maximum number of nearest points" ), Qgis::ProcessingNumberParameterType::Integer, 20, false, 1 );
270 maxPtsParam->setHelp( QObject::tr( "Maximum number of nearest points within the search distance to evaluate per grid cell." ) );
271 addParameter( maxPtsParam.release() );
272
273 auto minPtsParam = std::make_unique<QgsProcessingParameterNumber>( u"SEARCH_POINTS_MIN"_s, QObject::tr( "Minimum number of points" ), Qgis::ProcessingNumberParameterType::Integer, 16, false, 3 );
274 minPtsParam->setHelp( QObject::tr( "Minimum required points within search distance. At least 3 points are mandatory to solve a 2D spline; cells with fewer points are assigned NoData." ) );
275 addParameter( minPtsParam.release() );
276
277 addOutputParameters();
278}
279
280QVariantMap QgsLocalThinPlateSplineAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
281{
282 QGS_MARK_ALGORITHM_SOURCE
283
284 processBase( parameters, context, feedback );
285
286 const double searchRadius = parameterAsDouble( parameters, u"SEARCH_RADIUS"_s, context );
287 const int maxPoints = parameterAsInt( parameters, u"SEARCH_POINTS_MAX"_s, context );
288 const int minPoints = parameterAsInt( parameters, u"SEARCH_POINTS_MIN"_s, context );
289
290 QgsProcessingMultiStepFeedback multiStepFeedback( 2, feedback );
291 // estimate 5% of overall time for feature iteration and spatial index construction...
292 multiStepFeedback.setStepWeights( { 0.05, 0.95 } );
293
294 multiStepFeedback.setCurrentStep( 0 );
295 multiStepFeedback.pushInfo( QObject::tr( "Building spatial index…" ) );
296
297 FeatureZValueHash zValues;
298 QgsFeature feat;
299 QgsFeatureRequest request;
300 request.setSubsetOfAttributes( { mFieldIndex } );
301 QgsFeatureIterator fit = mSource->getFeatures( request );
302
303 const long count = mSource->featureCount();
304 const double step = count > 0 ? 100.0 / count : 1;
305
306 long long current = 0;
308 fit,
309 [&zValues, &current, &multiStepFeedback, step, this]( const QgsFeature &feature ) -> bool {
310 multiStepFeedback.setProgress( current * step );
311 current++;
312
313 if ( !feature.hasGeometry() )
314 return true;
315
316 const QVariant fieldValue = feature.attribute( mFieldIndex );
317 if ( !QgsVariantUtils::isNull( fieldValue ) )
318 {
319 zValues[feature.id()] = fieldValue.toDouble();
320 }
321 return true;
322 },
323 &multiStepFeedback
324 );
325
326 if ( multiStepFeedback.isCanceled() )
327 return {};
328
329 if ( zValues.size() < 3 )
330 {
331 throw QgsProcessingException( QObject::tr( "At least 3 valid points with non-null Z attributes are required to form a Thin Plate Spline." ) );
332 }
333
334 const FeatureZValueHash constZValues = std::as_const( zValues );
335
336 multiStepFeedback.setCurrentStep( 1 );
337 multiStepFeedback.pushInfo( QObject::tr( "Interpolating…" ) );
338
339 int cols = std::max( 1, static_cast<int>( std::ceil( mExtent.width() / mPixelSize ) ) );
340 int rows = std::max( 1, static_cast<int>( std::ceil( mExtent.height() / mPixelSize ) ) );
341
342 auto writer = std::make_unique<QgsRasterFileWriter>( mOutputPath );
343 writer->setOutputProviderKey( u"gdal"_s );
344 if ( !mCreationOptions.isEmpty() )
345 {
346 writer->setCreationOptions( mCreationOptions.split( '|' ) );
347 }
348 std::unique_ptr<QgsRasterDataProvider> provider( writer->createOneBandRaster( Qgis::DataType::Float32, cols, rows, mExtent, mSource->sourceCrs() ) );
349
350 if ( !provider )
351 throw QgsProcessingException( QObject::tr( "Could not write destination raster file: %1" ).arg( mOutputPath ) );
352 if ( !provider->isValid() )
353 throw QgsProcessingException( QObject::tr( "Could not create raster output %1: %2" ).arg( mOutputPath, provider->error().message( QgsErrorMessage::Text ) ) );
354
355 provider->setNoDataValue( 1, mNoDataValue );
356 provider->setEditable( true );
357
358 QgsRasterIterator iter( provider.get() );
359 iter.startRasterRead( 1, cols, rows, mExtent );
360
361 int iterCols = 0;
362 int iterRows = 0;
363 int topLeftCol = 0;
364 int topLeftRow = 0;
365 QgsRectangle blockExtent;
366
367 std::unique_ptr<QgsRasterBlock> outputBlock;
368
369 CellStats globalStats;
370
371 while ( iter.readNextRasterPart( 1, iterCols, iterRows, outputBlock, topLeftCol, topLeftRow, &blockExtent ) )
372 {
373 if ( multiStepFeedback.isCanceled() )
374 break;
375
376 QVector<int> tileRowIndices( iterRows );
377 std::iota( tileRowIndices.begin(), tileRowIndices.end(), 0 );
378
379 int completedTileRows = 0;
380
381 const CellStats tileStats = QtConcurrent::blockingMappedReduced<CellStats>(
382 tileRowIndices,
383 [iterCols, maxPoints, topLeftRow, topLeftCol, searchRadius, minPoints, &constZValues, &kdTree, &multiStepFeedback, this]( int r ) -> RowResult {
384 RowResult rowResult;
385 rowResult.r = r;
386 rowResult.values.resize( iterCols );
387
388 if ( multiStepFeedback.isCanceled() )
389 return rowResult;
390
391 QgsMatrixSolver solver( maxPoints + 3 );
392 QVector<double> W;
393 std::vector<Neighbor> neighbors;
394
395 const int globalRow = topLeftRow + r;
396 const double y = mExtent.yMaximum() - ( globalRow + 0.5 ) * mPixelSize;
397
398 for ( int c = 0; c < iterCols; ++c )
399 {
400 const int globalCol = topLeftCol + c;
401 const double x = mExtent.xMinimum() + ( globalCol + 0.5 ) * mPixelSize;
402 neighbors.clear();
403
404 kdTree.within( QgsPointXY( x, y ), searchRadius, [&constZValues, &neighbors, x, y]( const QgsSpatialIndexKDBushData &data ) {
405 const auto it = constZValues.constFind( data.id );
406 if ( it != constZValues.constEnd() )
407 {
408 const double pX = data.coords.first;
409 const double pY = data.coords.second;
410 const double dx = pX - x;
411 const double dy = pY - y;
412 neighbors.emplace_back( Neighbor { pX, pY, it.value(), dx * dx + dy * dy } );
413 }
414 } );
415
416 const std::size_t neighborCount = neighbors.size();
417 rowResult.stats.totalCells++;
418 rowResult.stats.minNeighbors = std::min( rowResult.stats.minNeighbors, neighborCount );
419 rowResult.stats.maxNeighbors = std::max( rowResult.stats.maxNeighbors, neighborCount );
420 rowResult.stats.sumNeighbors += neighborCount;
421
422 if ( static_cast< int >( neighborCount ) < minPoints )
423 {
424 rowResult.values[c] = mNoDataValue;
425 continue;
426 }
427
428 int n = static_cast< int >( neighborCount );
429 if ( n > maxPoints )
430 {
431 n = maxPoints;
432 std::partial_sort( neighbors.begin(), neighbors.begin() + n, neighbors.end() );
433 }
434
435 // distance statistics
436 for ( int i = 0; i < n; ++i )
437 {
438 const double dist = std::sqrt( neighbors[i].distSq );
439 rowResult.stats.minDistance = std::min( rowResult.stats.minDistance, dist );
440 rowResult.stats.maxDistance = std::max( rowResult.stats.maxDistance, dist );
441 rowResult.stats.sumDistance += dist;
442 rowResult.stats.countDistances++;
443 }
444
445 // Calculate Thin Plate Spline (TPS) weights from control points.
446 // System matrix L of size (n+3) x (n+3) consists of:
447 // - K (n x n, upper-left kernel matrix)
448 // - P (n x 3, upper-right polynomial matrix)
449 // - P^T (3 x n, lower-left transposed polynomial matrix)
450 // - O (3 x 3, lower-right zero matrix)
451 const int systemSize = n + 3;
452
453 // Fill K (n x n upper-left block of matrix L) and calculate mean edge length from control points.
454 // K is symmetric so we only compute the upper triangle and mirror values to the lower triangle.
455 double meanDist = 0.0;
456 for ( int i = 0; i < n; ++i )
457 {
458 const double iX = neighbors[i].x;
459 const double iY = neighbors[i].y;
460 for ( int j = i + 1; j < n; ++j )
461 {
462 const double dx = iX - neighbors[j].x;
463 const double dy = iY - neighbors[j].y;
464 const double distanceSquared = dx * dx + dy * dy;
465 const double distance = std::sqrt( distanceSquared );
466 const double baseVal = ( distance > 0.0 ) ? ( distanceSquared * std::log( distance ) ) : 0.0;
467 // Symmetric entries in K
468 meanDist += distance * 2.0;
469 solver.setValue( i, j, baseVal );
470 solver.setValue( j, i, baseVal );
471 }
472 }
473 meanDist /= ( n * n );
474
475 // Fill the remaining blocks of system matrix L and right-hand side vector v
476 for ( int i = 0; i < n; ++i )
477 {
478 // K diagonal: regularization parameters (lambda * meanDist^2)
479 solver.setValue( i, i, mRegularization * ( meanDist * meanDist ) );
480
481 // P (n x 3 upper-right block), P^T (3 x n bottom-left block)
482 solver.setValue( i, n, 1.0 );
483 solver.setValue( i, n + 1, neighbors[i].x );
484 solver.setValue( i, n + 2, neighbors[i].y );
485
486 solver.setValue( n, i, 1.0 );
487 solver.setValue( n + 1, i, neighbors[i].x );
488 solver.setValue( n + 2, i, neighbors[i].y );
489
490 // Fill right-hand side vector v (control point z values for 0 <= i < n)
491 solver.setRightHandSide( i, neighbors[i].z );
492 }
493
494 for ( int i = n; i < n + 3; ++i )
495 {
496 for ( int j = n; j < n + 3; ++j )
497 {
498 // O (3 x 3 lower-right block)
499 solver.setValue( i, j, 0.0 );
500 }
501 // Zero polynomial constraints for vector v
502 solver.setRightHandSide( i, 0.0 );
503 }
504
505 // NOTE: SAGA's version of this tool uses Lu solving only, but with a bug in the solver
506 // which prevents it correctly flagging singular matrices. Here we use the SVD fallback approach
507 // for a more tolerant solver, so that the results more closely represent SAGA's results (i.e.
508 // avoiding nodata pixels were SAGA's tools output data pixels)
509 if ( !solver.solve( systemSize, W, Qgis::LinearMatrixMethod::LuWithSvdFallback ) )
510 {
511 rowResult.values[c] = mNoDataValue;
512 rowResult.stats.unsolvableCells++;
513 continue;
514 }
515
516 rowResult.stats.validCells++;
517
518 // Evaluate local spline equation: z = a0 + ax*x + ay*y + sum(w_i * U(r_i))
519 double zVal = W[n] + W[n + 1] * x + W[n + 2] * y;
520 for ( int i = 0; i < n; ++i )
521 {
522 double distSq = neighbors[i].distSq;
523 double U = ( distSq > 0.0 ) ? ( distSq * 0.5 * std::log( distSq ) ) : 0.0;
524 zVal += W[i] * U;
525 }
526
527 rowResult.values[c] = zVal;
528 }
529
530 return rowResult;
531 },
532 [&completedTileRows, feedback, iterRows, iterCols, &outputBlock, &iter]( CellStats &accumulatedStats, const RowResult &rowResult ) {
533 accumulatedStats.merge( rowResult.stats );
534
535 for ( int c = 0; c < iterCols; ++c )
536 {
537 outputBlock->setValue( rowResult.r, c, rowResult.values[c] );
538 }
539
540 completedTileRows++;
541
542 const double currentBlockProgress = static_cast< double >( completedTileRows ) / iterRows;
543 const double blockProgressFraction = iter.progress( 1, currentBlockProgress );
544 const double overallProgress = 100.0 * ( 0.05 + 0.95 * blockProgressFraction );
545 feedback->setProgress( overallProgress );
546 }
547 );
548
549 globalStats.merge( tileStats );
550
551 if ( multiStepFeedback.isCanceled() )
552 break;
553
554 if ( !provider->writeBlock( outputBlock.get(), 1, topLeftCol, topLeftRow ) )
555 {
556 throw QgsProcessingException( QObject::tr( "Could not write raster block: %1" ).arg( provider->error().summary() ) );
557 }
558 multiStepFeedback.setProgress( 100.0 * iter.progress( 1 ) );
559 }
560 provider->setEditable( false );
561
562 iter.stopRasterRead( 1 );
563
564 if ( globalStats.unsolvableCells > 0 )
565 {
566 multiStepFeedback.pushWarning( QObject::tr( "The thin plate spline could not be solved for %1 raster cells. Try increasing the maximum search distance." ).arg( globalStats.unsolvableCells ) );
567 }
568
569 if ( globalStats.validCells > 0 )
570 {
571 const double meanNeighbors = static_cast< double >( globalStats.sumNeighbors ) / globalStats.totalCells;
572 multiStepFeedback.pushInfo( QObject::tr( "Neighbor count statistics:" ) );
573 multiStepFeedback.pushInfo( QObject::tr( "• Minimum: %1" ).arg( globalStats.minNeighbors ) );
574 multiStepFeedback.pushInfo( QObject::tr( "• Maximum: %1" ).arg( globalStats.maxNeighbors ) );
575 multiStepFeedback.pushInfo( QObject::tr( "• Mean: %1" ).arg( QString::number( meanNeighbors, 'f', 2 ) ) );
576 }
577
578 if ( globalStats.countDistances > 0 )
579 {
580 const double meanDistance = globalStats.sumDistance / globalStats.countDistances;
581 multiStepFeedback.pushInfo( QObject::tr( "Neighbor distance statistics:" ) );
582 multiStepFeedback.pushInfo( QObject::tr( "• Minimum: %1" ).arg( QString::number( globalStats.minDistance, 'f', 4 ) ) );
583 multiStepFeedback.pushInfo( QObject::tr( "• Maximum: %1" ).arg( QString::number( globalStats.maxDistance, 'f', 4 ) ) );
584 multiStepFeedback.pushInfo( QObject::tr( "• Mean: %1" ).arg( QString::number( meanDistance, 'f', 4 ) ) );
585 }
586
587 if ( static_cast< int >( globalStats.maxNeighbors ) < minPoints )
588 {
589 multiStepFeedback.pushWarning(
590 QObject::tr( "Maximum neighbors found within search radius was too small (got %1, required at least %2), output raster is empty" ).arg( globalStats.maxNeighbors ).arg( minPoints )
591 );
592 }
593
594 QVariantMap outputs;
595 outputs.insert( u"OUTPUT"_s, mOutputPath );
596 return outputs;
597}
598
599
600//
601// QgsGlobalThinPlateSplineAlgorithm
602//
603
604QgsGlobalThinPlateSplineAlgorithm::QgsGlobalThinPlateSplineAlgorithm() = default;
605
606QString QgsGlobalThinPlateSplineAlgorithm::name() const
607{
608 return u"globaltpsinterpolation"_s;
609}
610
611QString QgsGlobalThinPlateSplineAlgorithm::displayName() const
612{
613 return QObject::tr( "Thin Plate Spline interpolation (global)" );
614}
615
616QStringList QgsGlobalThinPlateSplineAlgorithm::tags() const
617{
618 return QObject::tr( "tps,thin,plate,spline,interpolation,surface" ).split( ',' );
619}
620
621QString QgsGlobalThinPlateSplineAlgorithm::shortDescription() const
622{
623 return QObject::tr( "Generates a Thin Plate Spline interpolation from scattered vector points." );
624}
625
626QString QgsGlobalThinPlateSplineAlgorithm::shortHelpString() const
627{
628 return QObject::tr(
629 "This algorithm calculates a single global Thin Plate Spline surface passing through all input points simultaneously.\n\n"
630 "Thin Plate Splines minimize the integral of the squared second derivatives, creating a smooth surface "
631 "resembling a bent thin metal plate. Regularisation allows softening the exact fitting constraint to smooth out noise.\n\n"
632 "A global Thin Plate Spline interpolation constructs and solves a single linear system across all control points up front. "
633 "It guarantees a continuous surface without spatial windowing boundaries, but requires high memory "
634 "and computation time for datasets with large point counts.\n\n"
635 "This algorithm is a port of the SAGA 'Thin Plate Spline' tool."
636 );
637}
638
639QgsGlobalThinPlateSplineAlgorithm *QgsGlobalThinPlateSplineAlgorithm::createInstance() const
640{
641 return new QgsGlobalThinPlateSplineAlgorithm();
642}
643
644void QgsGlobalThinPlateSplineAlgorithm::initAlgorithm( const QVariantMap & )
645{
646 addCommonParameters();
647 addOutputParameters();
648}
649
650namespace
651{
652 struct ControlPoint
653 {
654 double x = 0.0;
655 double y = 0.0;
656 double z = 0.0;
657 };
658} //namespace
659
660QVariantMap QgsGlobalThinPlateSplineAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
661{
662 QGS_MARK_ALGORITHM_SOURCE
663
664 processBase( parameters, context, feedback );
665
666 QgsProcessingMultiStepFeedback multiStepFeedback( 2, feedback );
667 // estimate 10% of overall time for feature iteration and matrix construction...
668 multiStepFeedback.setStepWeights( { 0.10, 0.90 } );
669
670 multiStepFeedback.setCurrentStep( 0 );
671 multiStepFeedback.pushInfo( QObject::tr( "Collecting control points and building global TPS matrix…" ) );
672
673 QgsFeature feat;
674 QgsFeatureRequest request;
675 request.setSubsetOfAttributes( { mFieldIndex } );
676 QgsFeatureIterator fit = mSource->getFeatures( request );
677
678 const long count = mSource->featureCount();
679 const double step = count > 0 ? 100.0 / count : 1;
680
681 std::vector<ControlPoint> globalPoints;
682
683 long long current = 0;
684 while ( fit.nextFeature( feat ) )
685 {
686 if ( multiStepFeedback.isCanceled() )
687 return {};
688
689 if ( feat.hasGeometry() )
690 {
691 const QVariant fieldValue = feat.attribute( mFieldIndex );
692 if ( !QgsVariantUtils::isNull( fieldValue ) )
693 {
694 const QgsPointXY pt = feat.geometry().asPoint();
695 globalPoints.push_back( ControlPoint { pt.x(), pt.y(), fieldValue.toDouble() } );
696 }
697 }
698 current++;
699 multiStepFeedback.setProgress( current * step );
700 }
701
702 if ( multiStepFeedback.isCanceled() )
703 return {};
704
705 if ( globalPoints.size() < 3 )
706 {
707 throw QgsProcessingException( QObject::tr( "At least 3 valid points with non-null Z attributes are required to form a Thin Plate Spline." ) );
708 }
709 const int n = static_cast<int>( globalPoints.size() );
710
711 constexpr double MAX_MEMORY_MB = 1024.0; // limit to max of 1gb memory
712 constexpr double MAX_MEMORY_BYTES = MAX_MEMORY_MB * 1024 * 1024;
713
714 const double matrixSizeBytes = ( static_cast<double>( n + 3 ) * ( n + 3 ) * sizeof( double ) );
715
716 if ( matrixSizeBytes > MAX_MEMORY_BYTES )
717 {
718 const int maxPointsAllowed = static_cast<int>( std::sqrt( MAX_MEMORY_BYTES / sizeof( double ) ) ) - 3;
719
721 QObject::tr(
722 "Global Thin Plate Spline failed: Input layer contains %1 points, requiring approximately %2 MB of RAM for matrix operations. "
723 "Global TPS solving is limited to %3 MB of RAM (approximately %4 points) to prevent memory allocation crashes. "
724 "Please use the 'Thin Plate Spline interpolation (local)' algorithm for large point layers."
725 )
726 .arg( n )
727 .arg( QString::number( matrixSizeBytes / ( 1024.0 * 1024.0 ), 'f', 1 ) )
728 .arg( MAX_MEMORY_MB )
729 .arg( maxPointsAllowed )
730 );
731 }
732
733 const int systemSize = n + 3;
734 QgsMatrixSolver globalSolver( systemSize );
735 QVector<double> globalW;
736
737 double meanDist = 0.0;
738 for ( int i = 0; i < n; ++i )
739 {
740 for ( int j = i + 1; j < n; ++j )
741 {
742 const double dx = globalPoints[i].x - globalPoints[j].x;
743 const double dy = globalPoints[i].y - globalPoints[j].y;
744 const double distSq = dx * dx + dy * dy;
745 const double dist = std::sqrt( distSq );
746 const double baseVal = ( distSq > 0.0 ) ? ( distSq * 0.5 * std::log( distSq ) ) : 0.0;
747 meanDist += dist * 2.0;
748 globalSolver.setValue( i, j, baseVal );
749 globalSolver.setValue( j, i, baseVal );
750 }
751 }
752 meanDist /= ( static_cast<double>( n ) * n );
753
754 // Populate regularization diagonal, polynomial terms P, and RHS z-vector
755 for ( int i = 0; i < n; ++i )
756 {
757 globalSolver.setValue( i, i, mRegularization * ( meanDist * meanDist ) );
758
759 globalSolver.setValue( i, n, 1.0 );
760 globalSolver.setValue( i, n + 1, globalPoints[i].x );
761 globalSolver.setValue( i, n + 2, globalPoints[i].y );
762
763 globalSolver.setValue( n, i, 1.0 );
764 globalSolver.setValue( n + 1, i, globalPoints[i].x );
765 globalSolver.setValue( n + 2, i, globalPoints[i].y );
766
767 globalSolver.setRightHandSide( i, globalPoints[i].z );
768 }
769 for ( int i = n; i < n + 3; ++i )
770 {
771 for ( int j = n; j < n + 3; ++j )
772 {
773 globalSolver.setValue( i, j, 0.0 );
774 }
775 globalSolver.setRightHandSide( i, 0.0 );
776 }
777
778 multiStepFeedback.pushInfo( QObject::tr( "Solving global linear system (%1 x %1)…" ).arg( systemSize ) );
779 // NOTE: SAGA's version of this tool uses Lu solving only, but with a bug in the solver
780 // which prevents it correctly flagging singular matrices. Here we use the SVD fallback approach
781 // for a more tolerant solver, so that the results more closely represent SAGA's results (i.e.
782 // avoiding nodata pixels were SAGA's tools output data pixels)
783 if ( !globalSolver.solve( systemSize, globalW, Qgis::LinearMatrixMethod::LuWithSvdFallback ) )
784 {
785 throw QgsProcessingException( QObject::tr( "Global matrix is singular and could not be solved." ) );
786 }
787
788 multiStepFeedback.setCurrentStep( 1 );
789 multiStepFeedback.pushInfo( QObject::tr( "Interpolating…" ) );
790
791 int cols = std::max( 1, static_cast<int>( std::ceil( mExtent.width() / mPixelSize ) ) );
792 int rows = std::max( 1, static_cast<int>( std::ceil( mExtent.height() / mPixelSize ) ) );
793
794 auto writer = std::make_unique<QgsRasterFileWriter>( mOutputPath );
795 writer->setOutputProviderKey( u"gdal"_s );
796 if ( !mCreationOptions.isEmpty() )
797 {
798 writer->setCreationOptions( mCreationOptions.split( '|' ) );
799 }
800 std::unique_ptr<QgsRasterDataProvider> provider( writer->createOneBandRaster( Qgis::DataType::Float32, cols, rows, mExtent, mSource->sourceCrs() ) );
801
802 if ( !provider )
803 throw QgsProcessingException( QObject::tr( "Could not write destination raster file: %1" ).arg( mOutputPath ) );
804 if ( !provider->isValid() )
805 throw QgsProcessingException( QObject::tr( "Could not create raster output %1: %2" ).arg( mOutputPath, provider->error().message( QgsErrorMessage::Text ) ) );
806
807 provider->setNoDataValue( 1, mNoDataValue );
808 provider->setEditable( true );
809
810 QgsRasterIterator iter( provider.get() );
811 iter.startRasterRead( 1, cols, rows, mExtent );
812
813 int iterCols = 0;
814 int iterRows = 0;
815 int topLeftCol = 0;
816 int topLeftRow = 0;
817 QgsRectangle blockExtent;
818
819 std::unique_ptr<QgsRasterBlock> outputBlock;
820 const std::vector<ControlPoint> &constGlobalPoints = std::as_const( globalPoints );
821 const double a0 = globalW[n];
822 const double ax = globalW[n + 1];
823 const double ay = globalW[n + 2];
824
825 while ( iter.readNextRasterPart( 1, iterCols, iterRows, outputBlock, topLeftCol, topLeftRow, &blockExtent ) )
826 {
827 if ( multiStepFeedback.isCanceled() )
828 break;
829
830 QVector<int> tileRowIndices( iterRows );
831 std::iota( tileRowIndices.begin(), tileRowIndices.end(), 0 );
832
833 int completedTileRows = 0;
834
835 QtConcurrent::blockingMappedReduced<int>(
836 tileRowIndices,
837 [iterCols, topLeftRow, topLeftCol, &multiStepFeedback, a0, ax, ay, n, &globalW, &constGlobalPoints, this]( int r ) -> RowResult {
838 RowResult rowResult;
839 rowResult.r = r;
840 rowResult.values.resize( iterCols );
841
842 if ( multiStepFeedback.isCanceled() )
843 return rowResult;
844
845 const int globalRow = topLeftRow + r;
846 const double y = mExtent.yMaximum() - ( globalRow + 0.5 ) * mPixelSize;
847
848 for ( int c = 0; c < iterCols; ++c )
849 {
850 const int globalCol = topLeftCol + c;
851 const double x = mExtent.xMinimum() + ( globalCol + 0.5 ) * mPixelSize;
852
853 double zVal = a0 + ax * x + ay * y;
854 for ( int i = 0; i < n; ++i )
855 {
856 const double dx = constGlobalPoints[i].x - x;
857 const double dy = constGlobalPoints[i].y - y;
858 const double distSq = dx * dx + dy * dy;
859 if ( distSq > 0.0 )
860 {
861 zVal += globalW[i] * ( distSq * 0.5 * std::log( distSq ) );
862 }
863 }
864 rowResult.values[c] = zVal;
865 }
866
867 return rowResult;
868 },
869 [&completedTileRows, feedback, iterRows, iterCols, &outputBlock, &iter]( int &, const RowResult &rowResult ) {
870 for ( int c = 0; c < iterCols; ++c )
871 {
872 outputBlock->setValue( rowResult.r, c, rowResult.values[c] );
873 }
874
875 completedTileRows++;
876
877 completedTileRows++;
878 const double currentBlockProgress = static_cast< double >( completedTileRows ) / iterRows;
879 feedback->setProgress( 100.0 * ( 0.10 + 0.90 * iter.progress( 1, currentBlockProgress ) ) );
880 }
881 );
882
883 if ( multiStepFeedback.isCanceled() )
884 break;
885
886 if ( !provider->writeBlock( outputBlock.get(), 1, topLeftCol, topLeftRow ) )
887 {
888 throw QgsProcessingException( QObject::tr( "Could not write raster block: %1" ).arg( provider->error().summary() ) );
889 }
890 multiStepFeedback.setProgress( 100.0 * iter.progress( 1 ) );
891 }
892 provider->setEditable( false );
893
894 iter.stopRasterRead( 1 );
895
896 QVariantMap outputs;
897 outputs.insert( u"OUTPUT"_s, mOutputPath );
898 return outputs;
899}
@ VectorPoint
Vector point layers.
Definition qgis.h:3752
@ Numeric
Accepts numeric fields.
Definition qgis.h:4039
@ Float32
Thirty two bit floating point (float).
Definition qgis.h:401
@ Advanced
Parameter is an advanced parameter which should be hidden from users by default.
Definition qgis.h:3984
@ LuWithSvdFallback
Try LU first; fallback to SVD on singularity.
Definition qgis.h:7165
@ Double
Double/float values.
Definition qgis.h:4025
Encapsulates an academic reference and formats it according to style guidelines.
static QgsAcademicReference createPresentation(const QStringList &authors, int year, const QString &title, const QString &meeting, const QString &publisher=QString(), const QString &pages=QString())
Creates a conference paper or presentation reference.
static QgsAcademicReference createWebPage(const QStringList &authors, int year, const QString &title, const QString &url)
Creates a web page or online resource reference.
static QIcon getThemeIcon(const QString &name, const QColor &fillColor=QColor(), const QColor &strokeColor=QColor())
Helper to get a theme icon.
static QString iconPath(const QString &iconFile)
Returns path to the desired icon file.
@ Text
Plain text format.
Definition qgserror.h:40
Wrapper for iterator of features from vector data provider or vector layer.
bool nextFeature(QgsFeature &f)
Fetch next feature and stores in f, returns true on success.
Wraps a request for features to a vector layer (or directly its vector data provider).
QgsFeatureRequest & setSubsetOfAttributes(const QgsAttributeList &attrs)
Set a subset of attributes that will be fetched.
The feature class encapsulates a single feature including its unique ID, geometry and a list of field...
Definition qgsfeature.h:60
QgsFeatureId id
Definition qgsfeature.h:63
QgsGeometry geometry
Definition qgsfeature.h:66
bool hasGeometry() const
Returns true if the feature has an associated geometry.
Q_INVOKABLE QVariant attribute(const QString &name) const
Lookup attribute value by attribute name.
QgsPointXY asPoint() const
Returns the contents of the geometry as a 2-dimensional point.
Contains utility functions for solving matrix operations.
static bool isAvailable()
Returns true if the matrix solver functionality is available on the current system.
Represents a 2D point.
Definition qgspointxy.h:62
double y
Definition qgspointxy.h:66
double x
Definition qgspointxy.h:65
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.
Processing feedback object for multi-step operations.
Iterator for sequentially processing raster cells.
A rectangle specified with double values.
A container for data stored inside a QgsSpatialIndexKDBush index.
std::pair< double, double > coords
Pair of coordinate data.
A very fast static spatial index for 2D points based on a flat KD-tree.
static bool isNull(const QVariant &variant, bool silenceNullWarnings=false)
Returns true if the specified variant should be considered a NULL value.
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 operator<(const QVariant &v1, const QVariant &v2)
Compares two QVariant values and returns whether the first is less than the second.
Definition qgis.h:8152