Skip to content

Commit 3f067fe

Browse files
committed
TPS transformer: check RAM available to avoid crashes / system freeze
1 parent f7896d9 commit 3f067fe

File tree

8 files changed

+212
-81
lines changed

8 files changed

+212
-81
lines changed

alg/gdal_tps.cpp

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
#include <stdlib.h>
1818
#include <string.h>
19+
20+
#include <algorithm>
1921
#include <map>
2022
#include <utility>
2123

@@ -25,6 +27,7 @@
2527
#include "cpl_minixml.h"
2628
#include "cpl_multiproc.h"
2729
#include "cpl_string.h"
30+
#include "cpl_vsi.h" // CPLGetUsablePhysicalRAM()
2831
#include "gdal.h"
2932
#include "gdal_alg.h"
3033
#include "gdal_alg_priv.h"
@@ -47,6 +50,7 @@ struct TPSTransformInfo
4750
double dfSrcApproxErrorReverse{};
4851

4952
bool bReversed{};
53+
bool bForceBuiltinMethod = false;
5054

5155
std::vector<gdal::GCP> asGCPs{};
5256

@@ -130,7 +134,8 @@ void *GDALCreateTPSTransformer(int nGCPCount, const GDAL_GCP *pasGCPList,
130134
static void GDALTPSComputeForwardInThread(void *pData)
131135
{
132136
TPSTransformInfo *psInfo = static_cast<TPSTransformInfo *>(pData);
133-
psInfo->bForwardSolved = psInfo->poForward->solve() != 0;
137+
psInfo->bForwardSolved =
138+
psInfo->poForward->solve(psInfo->bForceBuiltinMethod) != 0;
134139
}
135140

136141
void *GDALCreateTPSTransformerInt(int nGCPCount, const GDAL_GCP *pasGCPList,
@@ -233,6 +238,9 @@ void *GDALCreateTPSTransformerInt(int nGCPCount, const GDAL_GCP *pasGCPList,
233238
CSLFetchNameValueDef(papszOptions, "SRC_APPROX_ERROR_IN_PIXEL", "0"));
234239

235240
int nThreads = 1;
241+
bool bForceBuiltinMethod = false;
242+
// Arbitrary threshold beyond which multithreading might be interesting,
243+
// and checking memory usage too...
236244
if (nGCPCount > 100)
237245
{
238246
const char *pszWarpThreads =
@@ -243,23 +251,82 @@ void *GDALCreateTPSTransformerInt(int nGCPCount, const GDAL_GCP *pasGCPList,
243251
nThreads = CPLGetNumCPUs();
244252
else
245253
nThreads = atoi(pszWarpThreads);
254+
nThreads = std::clamp(nThreads, 1, 2);
255+
256+
// Do sanity checks w.r.t. available RAM
257+
258+
const auto nRAM = CPLGetUsablePhysicalRAM();
259+
if (nRAM > 0)
260+
{
261+
const int nMatrixSize = nGCPCount + 3;
262+
#ifdef HAVE_ARMADILLO
263+
// Armadillo requires up to 3 matrices of size nMatrixSize x nMatrixSize
264+
// for each transformation direction
265+
constexpr int NUM_TEMP_MATRICES = 3;
266+
if (nMatrixSize >
267+
nRAM / (nThreads * NUM_TEMP_MATRICES * nMatrixSize *
268+
static_cast<int>(sizeof(double))))
269+
{
270+
if (nMatrixSize > nRAM / (NUM_TEMP_MATRICES * nMatrixSize *
271+
static_cast<int>(sizeof(double))))
272+
{
273+
CPLDebug("GDAL", "Not enough memory to use Armadillo "
274+
"solver for thinplatespline. Falling back "
275+
"to LU decomposition method");
276+
bForceBuiltinMethod = true;
277+
}
278+
else
279+
{
280+
nThreads = 1;
281+
}
282+
}
283+
if (bForceBuiltinMethod)
284+
#endif
285+
{
286+
if (nMatrixSize > nRAM / (nThreads * nMatrixSize *
287+
static_cast<int>(sizeof(double))))
288+
{
289+
nThreads = 1;
290+
if (nMatrixSize >
291+
nRAM / (nMatrixSize * static_cast<int>(sizeof(double))))
292+
{
293+
CPLError(CE_Failure, CPLE_OutOfMemory,
294+
"thinplatespline: not enough memory. At least "
295+
"%u MB are required",
296+
static_cast<unsigned>(
297+
static_cast<uint64_t>(nMatrixSize) *
298+
nMatrixSize * sizeof(double) /
299+
(1024 * 1024)));
300+
GDALDestroyTPSTransformer(psInfo);
301+
return nullptr;
302+
}
303+
}
304+
}
305+
}
246306
}
247307

248-
if (nThreads > 1)
308+
psInfo->bForceBuiltinMethod = bForceBuiltinMethod;
309+
310+
if (nThreads == 2)
249311
{
250312
// Compute direct and reverse transforms in parallel.
251313
CPLJoinableThread *hThread =
252314
CPLCreateJoinableThread(GDALTPSComputeForwardInThread, psInfo);
253-
psInfo->bReverseSolved = psInfo->poReverse->solve() != 0;
315+
psInfo->bReverseSolved =
316+
psInfo->poReverse->solve(bForceBuiltinMethod) != 0;
254317
if (hThread != nullptr)
255318
CPLJoinThread(hThread);
256319
else
257-
psInfo->bForwardSolved = psInfo->poForward->solve() != 0;
320+
psInfo->bForwardSolved =
321+
psInfo->poForward->solve(bForceBuiltinMethod) != 0;
258322
}
259323
else
260324
{
261-
psInfo->bForwardSolved = psInfo->poForward->solve() != 0;
262-
psInfo->bReverseSolved = psInfo->poReverse->solve() != 0;
325+
psInfo->bForwardSolved =
326+
psInfo->poForward->solve(bForceBuiltinMethod) != 0;
327+
if (psInfo->bForwardSolved)
328+
psInfo->bReverseSolved =
329+
psInfo->poReverse->solve(bForceBuiltinMethod) != 0;
263330
}
264331

265332
if (!psInfo->bForwardSolved || !psInfo->bReverseSolved)

alg/gdallinearsystem.cpp

Lines changed: 59 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
#include <cassert>
3131
#include <cmath>
3232

33-
#ifndef HAVE_ARMADILLO
3433
namespace
3534
{
3635
// LU decomposition of the quadratic matrix A
@@ -47,8 +46,22 @@ bool solve(GDALMatrix &A, GDALMatrix &RHS, GDALMatrix &X, double eps)
4746
for (int iRow = 0; iRow < m; ++iRow)
4847
perm[iRow] = iRow;
4948

49+
// Arbitrary threshold to trigger progress in debug mode
50+
const bool bDebug = (m > 10000);
51+
int nLastPct = -1;
52+
5053
for (int step = 0; step < m - 1; ++step)
5154
{
55+
if (bDebug)
56+
{
57+
const int nPct = (step * 100 * 10 / m) / 2;
58+
if (nPct != nLastPct)
59+
{
60+
CPLDebug("GDAL", "solve(): %d.%d %%", nPct / 10, nPct % 10);
61+
nLastPct = nPct;
62+
}
63+
}
64+
5265
// determine pivot element
5366
int iMax = step;
5467
double dMax = std::abs(A(step, step));
@@ -91,6 +104,16 @@ bool solve(GDALMatrix &A, GDALMatrix &RHS, GDALMatrix &X, double eps)
91104
// LUP solve;
92105
for (int iCol = 0; iCol < n; ++iCol)
93106
{
107+
if (bDebug)
108+
{
109+
const int nPct = 500 + (iCol * 100 * 10 / n) / 2;
110+
if (nPct != nLastPct)
111+
{
112+
CPLDebug("GDAL", "solve(): %d.%d %%", nPct / 10, nPct % 10);
113+
nLastPct = nPct;
114+
}
115+
}
116+
94117
for (int iRow = 0; iRow < m; ++iRow)
95118
{
96119
X(iRow, iCol) = RHS(perm[iRow], iCol);
@@ -108,48 +131,60 @@ bool solve(GDALMatrix &A, GDALMatrix &RHS, GDALMatrix &X, double eps)
108131
X(iRow, iCol) /= A(iRow, iRow);
109132
}
110133
}
134+
135+
if (bDebug)
136+
{
137+
CPLDebug("GDAL", "solve(): 100.0 %%");
138+
}
139+
111140
return true;
112141
}
113142
} // namespace
114-
#endif
143+
115144
/************************************************************************/
116145
/* GDALLinearSystemSolve() */
117146
/* */
118147
/* Solves the linear system A*X_i = RHS_i for each column i */
119148
/* where A is a square matrix. */
120149
/************************************************************************/
121-
bool GDALLinearSystemSolve(GDALMatrix &A, GDALMatrix &RHS, GDALMatrix &X)
150+
bool GDALLinearSystemSolve(GDALMatrix &A, GDALMatrix &RHS, GDALMatrix &X,
151+
[[maybe_unused]] bool bForceBuiltinMethod)
122152
{
123153
assert(A.getNumRows() == RHS.getNumRows());
124154
assert(A.getNumCols() == X.getNumRows());
125155
assert(RHS.getNumCols() == X.getNumCols());
126-
try
127-
{
156+
128157
#ifdef HAVE_ARMADILLO
129-
arma::mat matA(A.data(), A.getNumRows(), A.getNumCols(), false, true);
130-
arma::mat matRHS(RHS.data(), RHS.getNumRows(), RHS.getNumCols(), false,
131-
true);
132-
arma::mat matOut(X.data(), X.getNumRows(), X.getNumCols(), false, true);
158+
if (!bForceBuiltinMethod)
159+
{
160+
try
161+
{
162+
arma::mat matA(A.data(), A.getNumRows(), A.getNumCols(), false,
163+
true);
164+
arma::mat matRHS(RHS.data(), RHS.getNumRows(), RHS.getNumCols(),
165+
false, true);
166+
arma::mat matOut(X.data(), X.getNumRows(), X.getNumCols(), false,
167+
true);
133168
#if ARMA_VERSION_MAJOR > 6 || \
134169
(ARMA_VERSION_MAJOR == 6 && ARMA_VERSION_MINOR >= 500)
135-
// Perhaps available in earlier versions, but didn't check
136-
return arma::solve(matOut, matA, matRHS,
137-
arma::solve_opts::equilibrate +
138-
arma::solve_opts::no_approx);
170+
// Perhaps available in earlier versions, but didn't check
171+
return arma::solve(matOut, matA, matRHS,
172+
arma::solve_opts::equilibrate +
173+
arma::solve_opts::no_approx);
139174
#else
140-
return arma::solve(matOut, matA, matRHS);
141-
#endif
142-
143-
#else // HAVE_ARMADILLO
144-
return solve(A, RHS, X, 0);
175+
return arma::solve(matOut, matA, matRHS);
145176
#endif
177+
}
178+
catch (std::exception const &e)
179+
{
180+
CPLError(CE_Failure, CPLE_AppDefined, "GDALLinearSystemSolve: %s",
181+
e.what());
182+
return false;
183+
}
146184
}
147-
catch (std::exception const &e)
148-
{
149-
CPLError(CE_Failure, CPLE_AppDefined, "GDALLinearSystemSolve: %s",
150-
e.what());
151-
return false;
152-
}
185+
#endif // HAVE_ARMADILLO
186+
187+
return solve(A, RHS, X, 0);
153188
}
154189

155190
/*! @endcond */

alg/gdallinearsystem.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ struct GDALMatrix
8484
std::vector<double> v;
8585
};
8686

87-
bool GDALLinearSystemSolve(GDALMatrix &A, GDALMatrix &RHS, GDALMatrix &X);
87+
bool GDALLinearSystemSolve(GDALMatrix &A, GDALMatrix &RHS, GDALMatrix &X,
88+
bool bForceBuiltinMethod = false);
8889

8990
#endif /* #ifndef GDALLINEARSYSTEM_H_INCLUDED */
9091

alg/gdaltransformer.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2083,7 +2083,8 @@ const char *GDALGetGenImgProjTranformerOptionList(void)
20832083
* </li>
20842084
* <li> MAX_GCP_ORDER: the maximum order to use for GCP derived polynomials if
20852085
* possible. The default is to autoselect based on the number of GCPs.
2086-
* A value of -1 triggers use of Thin Plate Spline instead of polynomials.
2086+
* A value of -1 triggers use of Thin Plate Spline instead of polynomials if
2087+
* SRC_METHOD/DST_METHOD is not specified.
20872088
* </li>
20882089
* <li>GCP_ANTIMERIDIAN_UNWRAP=AUTO/YES/NO. (GDAL &gt;= 3.8) Whether to
20892090
* "unwrap" longitudes of ground control points that span the antimeridian.
@@ -2100,6 +2101,8 @@ const char *GDALGetGenImgProjTranformerOptionList(void)
21002101
* method to be considered on the source dataset. Will be used for pixel/line
21012102
* to georef transformation on the source dataset. NO_GEOTRANSFORM can be
21022103
* used to specify the identity geotransform (ungeoreferenced image)
2104+
* Note that using GCP_TPS with more than a few thousand GCPs requires significant RAM usage
2105+
* (at least numGCPs * numGCPs * 8 bytes) and processing time.
21032106
* </li>
21042107
* <li> DST_METHOD: may have a value which is one of GEOTRANSFORM,
21052108
* GCP_POLYNOMIAL, GCP_HOMOGRAPHY, GCP_TPS, GEOLOC_ARRAY (added in 3.5), RPC to
@@ -2108,6 +2111,8 @@ const char *GDALGetGenImgProjTranformerOptionList(void)
21082111
* pixel/line to georef transformation on the destination dataset.
21092112
* NO_GEOTRANSFORM can be used to specify the identity geotransform
21102113
* (ungeoreferenced image)
2114+
* Note that using GCP_TPS with more than a few thousand GCPs requires significant RAM usage
2115+
* (at least numGCPs * numGCPs * 8 bytes) and processing time.
21112116
* </li>
21122117
* <li> RPC_HEIGHT: A fixed height to be used with RPC
21132118
* calculations. If RPC_HEIGHT and RPC_DEM are not specified but that the RPC

0 commit comments

Comments
 (0)