Coverage Report

Created: 2026-02-14 06:52

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/gdal/alg/gdal_tps.cpp
Line
Count
Source
1
/******************************************************************************
2
 *
3
 * Project:  High Performance Image Reprojector
4
 * Purpose:  Thin Plate Spline transformer (GDAL wrapper portion)
5
 * Author:   Frank Warmerdam, warmerdam@pobox.com
6
 *
7
 ******************************************************************************
8
 * Copyright (c) 2004, Frank Warmerdam <warmerdam@pobox.com>
9
 * Copyright (c) 2011-2013, Even Rouault <even dot rouault at spatialys.com>
10
 *
11
 * SPDX-License-Identifier: MIT
12
 ****************************************************************************/
13
14
#include "cpl_port.h"
15
#include "thinplatespline.h"
16
17
#include <stdlib.h>
18
#include <string.h>
19
20
#include <algorithm>
21
#include <map>
22
#include <utility>
23
24
#include "cpl_atomic_ops.h"
25
#include "cpl_conv.h"
26
#include "cpl_error.h"
27
#include "cpl_minixml.h"
28
#include "cpl_multiproc.h"
29
#include "cpl_string.h"
30
#include "cpl_vsi.h"  // CPLGetUsablePhysicalRAM()
31
#include "gdal.h"
32
#include "gdal_alg.h"
33
#include "gdal_alg_priv.h"
34
#include "gdal_priv.h"
35
#include "gdalgenericinverse.h"
36
#include "gdal_thread_pool.h"
37
38
CPL_C_START
39
CPLXMLNode *GDALSerializeTPSTransformer(void *pTransformArg);
40
void *GDALDeserializeTPSTransformer(CPLXMLNode *psTree);
41
CPL_C_END
42
43
struct TPSTransformInfo
44
{
45
    GDALTransformerInfo sTI{};
46
47
    VizGeorefSpline2D *poForward{};
48
    VizGeorefSpline2D *poReverse{};
49
    bool bForwardSolved{};
50
    bool bReverseSolved{};
51
    double dfSrcApproxErrorReverse{};
52
53
    bool bReversed{};
54
    bool bForceBuiltinMethod = false;
55
56
    std::vector<gdal::GCP> asGCPs{};
57
58
    volatile int nRefCount{};
59
};
60
61
/************************************************************************/
62
/*                  GDALCreateSimilarTPSTransformer()                   */
63
/************************************************************************/
64
65
static void *GDALCreateSimilarTPSTransformer(void *hTransformArg,
66
                                             double dfRatioX, double dfRatioY)
67
0
{
68
0
    VALIDATE_POINTER1(hTransformArg, "GDALCreateSimilarTPSTransformer",
69
0
                      nullptr);
70
71
0
    TPSTransformInfo *psInfo = static_cast<TPSTransformInfo *>(hTransformArg);
72
73
0
    if (dfRatioX == 1.0 && dfRatioY == 1.0)
74
0
    {
75
        // We can just use a ref count, since using the source transformation
76
        // is thread-safe.
77
0
        CPLAtomicInc(&(psInfo->nRefCount));
78
0
    }
79
0
    else
80
0
    {
81
0
        auto newGCPs = psInfo->asGCPs;
82
0
        for (auto &gcp : newGCPs)
83
0
        {
84
0
            gcp.Pixel() /= dfRatioX;
85
0
            gcp.Line() /= dfRatioY;
86
0
        }
87
0
        psInfo = static_cast<TPSTransformInfo *>(GDALCreateTPSTransformer(
88
0
            static_cast<int>(newGCPs.size()), gdal::GCP::c_ptr(newGCPs),
89
0
            psInfo->bReversed));
90
0
    }
91
92
0
    return psInfo;
93
0
}
94
95
/************************************************************************/
96
/*                      GDALCreateTPSTransformer()                      */
97
/************************************************************************/
98
99
/**
100
 * Create Thin Plate Spline transformer from GCPs.
101
 *
102
 * The thin plate spline transformer produces exact transformation
103
 * at all control points and smoothly varying transformations between
104
 * control points with greatest influence from local control points.
105
 * It is suitable for for many applications not well modeled by polynomial
106
 * transformations.
107
 *
108
 * Creating the TPS transformer involves solving systems of linear equations
109
 * related to the number of control points involved.  This solution is
110
 * computed within this function call.  It can be quite an expensive operation
111
 * for large numbers of GCPs.  For instance, for reference, it takes on the
112
 * order of 10s for 400 GCPs on a 2GHz Athlon processor.
113
 *
114
 * TPS Transformers are serializable.
115
 *
116
 * The GDAL Thin Plate Spline transformer is based on code provided by
117
 * Gilad Ronnen on behalf of VIZRT Inc (http://www.visrt.com).  Incorporation
118
 * of the algorithm into GDAL was supported by the Centro di Ecologia Alpina
119
 * (http://www.cealp.it).
120
 *
121
 * @param nGCPCount the number of GCPs in pasGCPList.
122
 * @param pasGCPList an array of GCPs to be used as input.
123
 * @param bReversed set it to TRUE to compute the reversed transformation.
124
 *
125
 * @return the transform argument or NULL if creation fails.
126
 */
127
128
void *GDALCreateTPSTransformer(int nGCPCount, const GDAL_GCP *pasGCPList,
129
                               int bReversed)
130
0
{
131
0
    return GDALCreateTPSTransformerInt(nGCPCount, pasGCPList, bReversed,
132
0
                                       nullptr);
133
0
}
134
135
static void GDALTPSComputeForwardInThread(void *pData)
136
0
{
137
0
    TPSTransformInfo *psInfo = static_cast<TPSTransformInfo *>(pData);
138
0
    psInfo->bForwardSolved =
139
0
        psInfo->poForward->solve(psInfo->bForceBuiltinMethod) != 0;
140
0
}
141
142
void *GDALCreateTPSTransformerInt(int nGCPCount, const GDAL_GCP *pasGCPList,
143
                                  int bReversed, CSLConstList papszOptions)
144
145
0
{
146
    /* -------------------------------------------------------------------- */
147
    /*      Allocate transform info.                                        */
148
    /* -------------------------------------------------------------------- */
149
0
    TPSTransformInfo *psInfo = new TPSTransformInfo();
150
151
0
    psInfo->asGCPs = gdal::GCP::fromC(pasGCPList, nGCPCount);
152
153
0
    psInfo->bReversed = CPL_TO_BOOL(bReversed);
154
0
    psInfo->poForward = new VizGeorefSpline2D(2);
155
0
    psInfo->poReverse = new VizGeorefSpline2D(2);
156
157
0
    memcpy(psInfo->sTI.abySignature, GDAL_GTI2_SIGNATURE,
158
0
           strlen(GDAL_GTI2_SIGNATURE));
159
0
    psInfo->sTI.pszClassName = "GDALTPSTransformer";
160
0
    psInfo->sTI.pfnTransform = GDALTPSTransform;
161
0
    psInfo->sTI.pfnCleanup = GDALDestroyTPSTransformer;
162
0
    psInfo->sTI.pfnSerialize = GDALSerializeTPSTransformer;
163
0
    psInfo->sTI.pfnCreateSimilar = GDALCreateSimilarTPSTransformer;
164
165
    /* -------------------------------------------------------------------- */
166
    /*      Attach (non-redundant) points to the transformation.            */
167
    /* -------------------------------------------------------------------- */
168
0
    std::map<std::pair<double, double>, int> oMapPixelLineToIdx;
169
0
    std::map<std::pair<double, double>, int> oMapXYToIdx;
170
0
    for (int iGCP = 0; iGCP < nGCPCount; iGCP++)
171
0
    {
172
0
        const double afPL[2] = {pasGCPList[iGCP].dfGCPPixel,
173
0
                                pasGCPList[iGCP].dfGCPLine};
174
0
        const double afXY[2] = {pasGCPList[iGCP].dfGCPX,
175
0
                                pasGCPList[iGCP].dfGCPY};
176
177
0
        std::map<std::pair<double, double>, int>::iterator oIter(
178
0
            oMapPixelLineToIdx.find(
179
0
                std::pair<double, double>(afPL[0], afPL[1])));
180
0
        if (oIter != oMapPixelLineToIdx.end())
181
0
        {
182
0
            if (afXY[0] == pasGCPList[oIter->second].dfGCPX &&
183
0
                afXY[1] == pasGCPList[oIter->second].dfGCPY)
184
0
            {
185
0
                continue;
186
0
            }
187
0
            else
188
0
            {
189
0
                CPLError(CE_Warning, CPLE_AppDefined,
190
0
                         "GCP %d and %d have same (pixel,line)=(%f,%f), "
191
0
                         "but different (X,Y): (%f,%f) vs (%f,%f)",
192
0
                         iGCP + 1, oIter->second, afPL[0], afPL[1], afXY[0],
193
0
                         afXY[1], pasGCPList[oIter->second].dfGCPX,
194
0
                         pasGCPList[oIter->second].dfGCPY);
195
0
            }
196
0
        }
197
0
        else
198
0
        {
199
0
            oMapPixelLineToIdx[std::pair<double, double>(afPL[0], afPL[1])] =
200
0
                iGCP;
201
0
        }
202
203
0
        oIter = oMapXYToIdx.find(std::pair<double, double>(afXY[0], afXY[1]));
204
0
        if (oIter != oMapXYToIdx.end())
205
0
        {
206
0
            CPLError(CE_Warning, CPLE_AppDefined,
207
0
                     "GCP %d and %d have same (x,y)=(%f,%f), "
208
0
                     "but different (pixel,line): (%f,%f) vs (%f,%f)",
209
0
                     iGCP + 1, oIter->second, afXY[0], afXY[1], afPL[0],
210
0
                     afPL[1], pasGCPList[oIter->second].dfGCPPixel,
211
0
                     pasGCPList[oIter->second].dfGCPLine);
212
0
        }
213
0
        else
214
0
        {
215
0
            oMapXYToIdx[std::pair<double, double>(afXY[0], afXY[1])] = iGCP;
216
0
        }
217
218
0
        bool bOK = true;
219
0
        if (bReversed)
220
0
        {
221
0
            bOK &= psInfo->poReverse->add_point(afPL[0], afPL[1], afXY);
222
0
            bOK &= psInfo->poForward->add_point(afXY[0], afXY[1], afPL);
223
0
        }
224
0
        else
225
0
        {
226
0
            bOK &= psInfo->poForward->add_point(afPL[0], afPL[1], afXY);
227
0
            bOK &= psInfo->poReverse->add_point(afXY[0], afXY[1], afPL);
228
0
        }
229
0
        if (!bOK)
230
0
        {
231
0
            GDALDestroyTPSTransformer(psInfo);
232
0
            return nullptr;
233
0
        }
234
0
    }
235
236
0
    psInfo->nRefCount = 1;
237
238
0
    psInfo->dfSrcApproxErrorReverse = CPLAtof(
239
0
        CSLFetchNameValueDef(papszOptions, "SRC_APPROX_ERROR_IN_PIXEL", "0"));
240
241
0
    int nThreads = 1;
242
0
    bool bForceBuiltinMethod = false;
243
    // Arbitrary threshold beyond which multithreading might be interesting,
244
    // and checking memory usage too...
245
0
    if (nGCPCount > 100)
246
0
    {
247
        // We don't need more than 2 threads: one for forward transformation,
248
        // and another one for reverse transformation.
249
0
        nThreads = GDALGetNumThreads(papszOptions, "NUM_THREADS",
250
0
                                     /* nMaxVal = */ 2,
251
0
                                     /* bDefaultAllCPUs = */ false);
252
253
        // Do sanity checks w.r.t. available RAM
254
255
0
        const auto nRAM = CPLGetUsablePhysicalRAM();
256
0
        if (nRAM > 0)
257
0
        {
258
0
            const int nMatrixSize = nGCPCount + 3;
259
#ifdef HAVE_ARMADILLO
260
            // Armadillo requires up to 3 matrices of size nMatrixSize x nMatrixSize
261
            // for each transformation direction
262
            constexpr int NUM_TEMP_MATRICES = 3;
263
            if (nMatrixSize >
264
                nRAM / (nThreads * NUM_TEMP_MATRICES * nMatrixSize *
265
                        static_cast<int>(sizeof(double))))
266
            {
267
                if (nMatrixSize > nRAM / (NUM_TEMP_MATRICES * nMatrixSize *
268
                                          static_cast<int>(sizeof(double))))
269
                {
270
                    CPLDebug("GDAL", "Not enough memory to use Armadillo "
271
                                     "solver for thinplatespline. Falling back "
272
                                     "to LU decomposition method");
273
                    bForceBuiltinMethod = true;
274
                }
275
                else
276
                {
277
                    nThreads = 1;
278
                }
279
            }
280
            if (bForceBuiltinMethod)
281
#endif
282
0
            {
283
0
                if (nMatrixSize > nRAM / (nThreads * nMatrixSize *
284
0
                                          static_cast<int>(sizeof(double))))
285
0
                {
286
0
                    nThreads = 1;
287
0
                    if (nMatrixSize >
288
0
                        nRAM / (nMatrixSize * static_cast<int>(sizeof(double))))
289
0
                    {
290
0
                        CPLError(CE_Failure, CPLE_OutOfMemory,
291
0
                                 "thinplatespline: not enough memory. At least "
292
0
                                 "%u MB are required",
293
0
                                 static_cast<unsigned>(
294
0
                                     static_cast<uint64_t>(nMatrixSize) *
295
0
                                     nMatrixSize * sizeof(double) /
296
0
                                     (1024 * 1024)));
297
0
                        GDALDestroyTPSTransformer(psInfo);
298
0
                        return nullptr;
299
0
                    }
300
0
                }
301
0
            }
302
0
        }
303
0
    }
304
305
0
    psInfo->bForceBuiltinMethod = bForceBuiltinMethod;
306
307
0
    if (nThreads == 2)
308
0
    {
309
        // Compute direct and reverse transforms in parallel.
310
0
        CPLJoinableThread *hThread =
311
0
            CPLCreateJoinableThread(GDALTPSComputeForwardInThread, psInfo);
312
0
        psInfo->bReverseSolved =
313
0
            psInfo->poReverse->solve(bForceBuiltinMethod) != 0;
314
0
        if (hThread != nullptr)
315
0
            CPLJoinThread(hThread);
316
0
        else
317
0
            psInfo->bForwardSolved =
318
0
                psInfo->poForward->solve(bForceBuiltinMethod) != 0;
319
0
    }
320
0
    else
321
0
    {
322
0
        psInfo->bForwardSolved =
323
0
            psInfo->poForward->solve(bForceBuiltinMethod) != 0;
324
0
        if (psInfo->bForwardSolved)
325
0
            psInfo->bReverseSolved =
326
0
                psInfo->poReverse->solve(bForceBuiltinMethod) != 0;
327
0
    }
328
329
0
    if (!psInfo->bForwardSolved || !psInfo->bReverseSolved)
330
0
    {
331
0
        GDALDestroyTPSTransformer(psInfo);
332
0
        return nullptr;
333
0
    }
334
335
0
    return psInfo;
336
0
}
337
338
/************************************************************************/
339
/*                     GDALDestroyTPSTransformer()                      */
340
/************************************************************************/
341
342
/**
343
 * Destroy TPS transformer.
344
 *
345
 * This function is used to destroy information about a GCP based
346
 * polynomial transformation created with GDALCreateTPSTransformer().
347
 *
348
 * @param pTransformArg the transform arg previously returned by
349
 * GDALCreateTPSTransformer().
350
 */
351
352
void GDALDestroyTPSTransformer(void *pTransformArg)
353
354
0
{
355
0
    if (pTransformArg == nullptr)
356
0
        return;
357
358
0
    TPSTransformInfo *psInfo = static_cast<TPSTransformInfo *>(pTransformArg);
359
360
0
    if (CPLAtomicDec(&(psInfo->nRefCount)) == 0)
361
0
    {
362
0
        delete psInfo->poForward;
363
0
        delete psInfo->poReverse;
364
365
0
        delete psInfo;
366
0
    }
367
0
}
368
369
/************************************************************************/
370
/*                          GDALTPSTransform()                          */
371
/************************************************************************/
372
373
/**
374
 * Transforms point based on GCP derived polynomial model.
375
 *
376
 * This function matches the GDALTransformerFunc signature, and can be
377
 * used to transform one or more points from pixel/line coordinates to
378
 * georeferenced coordinates (SrcToDst) or vice versa (DstToSrc).
379
 *
380
 * @param pTransformArg return value from GDALCreateTPSTransformer().
381
 * @param bDstToSrc TRUE if transformation is from the destination
382
 * (georeferenced) coordinates to pixel/line or FALSE when transforming
383
 * from pixel/line to georeferenced coordinates.
384
 * @param nPointCount the number of values in the x, y and z arrays.
385
 * @param x array containing the X values to be transformed.
386
 * @param y array containing the Y values to be transformed.
387
 * @param z array containing the Z values to be transformed.
388
 * @param panSuccess array in which a flag indicating success (TRUE) or
389
 * failure (FALSE) of the transformation are placed.
390
 *
391
 * @return TRUE if all points have been successfully transformed.
392
 */
393
394
int GDALTPSTransform(void *pTransformArg, int bDstToSrc, int nPointCount,
395
                     double *x, double *y, CPL_UNUSED double *z,
396
                     int *panSuccess)
397
0
{
398
0
    VALIDATE_POINTER1(pTransformArg, "GDALTPSTransform", 0);
399
400
0
    TPSTransformInfo *psInfo = static_cast<TPSTransformInfo *>(pTransformArg);
401
402
0
    for (int i = 0; i < nPointCount; i++)
403
0
    {
404
0
        double xy_out[2] = {0.0, 0.0};
405
406
0
        if (bDstToSrc)
407
0
        {
408
            // Compute initial guess
409
0
            psInfo->poReverse->get_point(x[i], y[i], xy_out);
410
411
0
            const auto ForwardTransformer = [](double xIn, double yIn,
412
0
                                               double &xOut, double &yOut,
413
0
                                               void *pUserData)
414
0
            {
415
0
                double xyOut[2] = {0.0, 0.0};
416
0
                TPSTransformInfo *l_psInfo =
417
0
                    static_cast<TPSTransformInfo *>(pUserData);
418
0
                l_psInfo->poForward->get_point(xIn, yIn, xyOut);
419
0
                xOut = xyOut[0];
420
0
                yOut = xyOut[1];
421
0
                return true;
422
0
            };
423
424
            // Refine the initial guess
425
0
            GDALGenericInverse2D(
426
0
                x[i], y[i], xy_out[0], xy_out[1], ForwardTransformer, psInfo,
427
0
                xy_out[0], xy_out[1],
428
0
                /* computeJacobianMatrixOnlyAtFirstIter = */ true,
429
0
                /* toleranceOnOutputCoordinates = */ 0,
430
0
                psInfo->dfSrcApproxErrorReverse);
431
0
            x[i] = xy_out[0];
432
0
            y[i] = xy_out[1];
433
0
        }
434
0
        else
435
0
        {
436
0
            psInfo->poForward->get_point(x[i], y[i], xy_out);
437
0
            x[i] = xy_out[0];
438
0
            y[i] = xy_out[1];
439
0
        }
440
0
        panSuccess[i] = TRUE;
441
0
    }
442
443
0
    return TRUE;
444
0
}
445
446
/************************************************************************/
447
/*                    GDALSerializeTPSTransformer()                     */
448
/************************************************************************/
449
450
CPLXMLNode *GDALSerializeTPSTransformer(void *pTransformArg)
451
452
0
{
453
0
    VALIDATE_POINTER1(pTransformArg, "GDALSerializeTPSTransformer", nullptr);
454
455
0
    TPSTransformInfo *psInfo = static_cast<TPSTransformInfo *>(pTransformArg);
456
457
0
    CPLXMLNode *psTree =
458
0
        CPLCreateXMLNode(nullptr, CXT_Element, "TPSTransformer");
459
460
    /* -------------------------------------------------------------------- */
461
    /*      Serialize bReversed.                                            */
462
    /* -------------------------------------------------------------------- */
463
0
    CPLCreateXMLElementAndValue(
464
0
        psTree, "Reversed",
465
0
        CPLString().Printf("%d", static_cast<int>(psInfo->bReversed)));
466
467
    /* -------------------------------------------------------------------- */
468
    /*      Attach GCP List.                                                */
469
    /* -------------------------------------------------------------------- */
470
0
    if (!psInfo->asGCPs.empty())
471
0
    {
472
0
        GDALSerializeGCPListToXML(psTree, psInfo->asGCPs, nullptr);
473
0
    }
474
475
0
    if (psInfo->dfSrcApproxErrorReverse > 0)
476
0
    {
477
0
        CPLCreateXMLElementAndValue(
478
0
            psTree, "SrcApproxErrorInPixel",
479
0
            CPLString().Printf("%g", psInfo->dfSrcApproxErrorReverse));
480
0
    }
481
482
0
    return psTree;
483
0
}
484
485
/************************************************************************/
486
/*                   GDALDeserializeTPSTransformer()                    */
487
/************************************************************************/
488
489
void *GDALDeserializeTPSTransformer(CPLXMLNode *psTree)
490
491
0
{
492
    /* -------------------------------------------------------------------- */
493
    /*      Check for GCPs.                                                 */
494
    /* -------------------------------------------------------------------- */
495
0
    CPLXMLNode *psGCPList = CPLGetXMLNode(psTree, "GCPList");
496
497
0
    std::vector<gdal::GCP> asGCPs;
498
0
    if (psGCPList != nullptr)
499
0
    {
500
0
        GDALDeserializeGCPListFromXML(psGCPList, asGCPs, nullptr);
501
0
    }
502
503
    /* -------------------------------------------------------------------- */
504
    /*      Get other flags.                                                */
505
    /* -------------------------------------------------------------------- */
506
0
    const int bReversed = atoi(CPLGetXMLValue(psTree, "Reversed", "0"));
507
508
0
    CPLStringList aosOptions;
509
0
    aosOptions.SetNameValue(
510
0
        "SRC_APPROX_ERROR_IN_PIXEL",
511
0
        CPLGetXMLValue(psTree, "SrcApproxErrorInPixel", nullptr));
512
513
    /* -------------------------------------------------------------------- */
514
    /*      Generate transformation.                                        */
515
    /* -------------------------------------------------------------------- */
516
0
    void *pResult = GDALCreateTPSTransformerInt(static_cast<int>(asGCPs.size()),
517
0
                                                gdal::GCP::c_ptr(asGCPs),
518
0
                                                bReversed, aosOptions.List());
519
520
0
    return pResult;
521
0
}