Line data Source code
1 : /******************************************************************************
2 : *
3 : * Project: GDAL Utilities
4 : * Purpose: Convert nearly black or nearly white border to exact black/white
5 : * using the flood fill algorithm.
6 : * Author: Even Rouault <even dot rouault at spatialys.com>
7 : *
8 : * ****************************************************************************
9 : * Copyright (c) 2023, Even Rouault <even dot rouault at spatialys.com>
10 : *
11 : * SPDX-License-Identifier: MIT
12 : ****************************************************************************/
13 :
14 : #include "gdal_priv.h"
15 : #include "nearblack_lib.h"
16 :
17 : #include <algorithm>
18 : #include <memory>
19 : #include <queue>
20 :
21 : /************************************************************************/
22 : /* GDALNearblackFloodFillAlg */
23 : /************************************************************************/
24 :
25 : // Implements the "final, combined-scan-and-fill span filler was then published
26 : // in 1990" algorithm of https://en.wikipedia.org/wiki/Flood_fill#Span_filling
27 :
28 : struct GDALNearblackFloodFillAlg
29 : {
30 : // Input arguments of the algorithm
31 : const GDALNearblackOptions *m_psOptions = nullptr;
32 : GDALDataset *m_poSrcDataset = nullptr;
33 : GDALDataset *m_poDstDS = nullptr;
34 : GDALRasterBand *m_poMaskBand = nullptr;
35 : int m_nSrcBands = 0;
36 : int m_nDstBands = 0;
37 : bool m_bSetMask = false;
38 : Colors m_oColors{};
39 : GByte m_nReplacevalue = 0;
40 :
41 : // As we (generally) do not modify the value of pixels that are "black"
42 : // we need to keep track of the pixels we visited
43 : // Cf https://en.wikipedia.org/wiki/Flood_fill#Disadvantages_2
44 : // and https://en.wikipedia.org/wiki/Flood_fill#Adding_pattern_filling_support
45 : // for the requirement to add that extra sentinel
46 : std::unique_ptr<GDALDataset> m_poVisitedDS = nullptr;
47 :
48 : // Active line for the m_abyLine, m_abyLineMustSet, m_abyMask buffers
49 : int m_nLoadedLine = -1;
50 :
51 : // Whether Set(..., m_nLoadedLine) has been called
52 : bool m_bLineModified = true;
53 :
54 : // Content of m_poSrcDataset/m_poDstDS for m_nLoadedLine
55 : // Contains m_nDstBands * nXSize values in the order (R,G,B),(R,G,B),...
56 : std::vector<GByte> m_abyLine{};
57 :
58 : static constexpr GByte MUST_FILL_UNINIT = 0; // must be 0
59 : static constexpr GByte MUST_FILL_FALSE = 1;
60 : static constexpr GByte MUST_FILL_TRUE = 2;
61 : // Content of m_poVisitedDS for m_nLoadedLine
62 : std::vector<GByte> m_abyLineMustSet{};
63 :
64 : // Only use if m_bSetMask
65 : std::vector<GByte> m_abyMask{};
66 :
67 : // Used for progress bar. Incremented the first time a line ifs loaded
68 : int m_nCountLoadedOnce = 0;
69 :
70 : // m_abLineLoadedOnce[line] is set to true after the first time the line
71 : // of m_poSrcDataset is loaded by LoadLine(line)
72 : std::vector<bool> m_abLineLoadedOnce{};
73 :
74 : // m_abLineSavedOnce[line] is set to true after the first time the line
75 : // of m_poDstDS is written by LoadLine()
76 : std::vector<bool> m_abLineSavedOnce{};
77 :
78 : #ifdef DEBUG
79 : size_t m_nMaxQueueSize = 0;
80 : #endif
81 :
82 : // Entry point
83 : bool Process();
84 :
85 : private:
86 : bool Fill(int iX, int iY);
87 : bool LoadLine(int iY);
88 : bool MustSet(int iX, int iY);
89 : void Set(int iX, int iY);
90 : };
91 :
92 : /************************************************************************/
93 : /* GDALNearblackFloodFillAlg::MustSet() */
94 : /* */
95 : /* Called Inside() in https://en.wikipedia.org/wiki/Flood_fill */
96 : /************************************************************************/
97 :
98 : // Returns true if the pixel (iX, iY) is "black" (or more generally transparent
99 : // according to m_oColors)
100 9281 : bool GDALNearblackFloodFillAlg::MustSet(int iX, int iY)
101 : {
102 9281 : CPLAssert(iX >= 0);
103 9281 : CPLAssert(iX < m_poSrcDataset->GetRasterXSize());
104 :
105 9281 : CPLAssert(iY >= 0);
106 9281 : CPLAssert(iY < m_poSrcDataset->GetRasterYSize());
107 9281 : CPLAssert(iY == m_nLoadedLine);
108 9281 : CPL_IGNORE_RET_VAL(iY);
109 :
110 9281 : if (m_abyLineMustSet[iX] != MUST_FILL_UNINIT)
111 : {
112 1300 : return m_abyLineMustSet[iX] == MUST_FILL_TRUE;
113 : }
114 :
115 : /***** loop over the colors *****/
116 :
117 10965 : for (int iColor = 0; iColor < static_cast<int>(m_oColors.size()); iColor++)
118 : {
119 9617 : const Color &oColor = m_oColors[iColor];
120 :
121 : /***** loop over the bands *****/
122 9617 : bool bIsNonBlack = false;
123 :
124 29267 : for (int iBand = 0; iBand < m_nSrcBands; iBand++)
125 : {
126 22634 : const int nPix = m_abyLine[iX * m_nDstBands + iBand];
127 :
128 45004 : if (oColor[iBand] - nPix > m_psOptions->nNearDist ||
129 22370 : nPix > m_psOptions->nNearDist + oColor[iBand])
130 : {
131 2984 : bIsNonBlack = true;
132 2984 : break;
133 : }
134 : }
135 :
136 9617 : if (!bIsNonBlack)
137 : {
138 6633 : m_abyLineMustSet[iX] = MUST_FILL_TRUE;
139 6633 : return true;
140 : }
141 : }
142 :
143 1348 : m_abyLineMustSet[iX] = MUST_FILL_FALSE;
144 1348 : return false;
145 : }
146 :
147 : /************************************************************************/
148 : /* GDALNearblackFloodFillAlg::LoadLine() */
149 : /************************************************************************/
150 :
151 : // Load the new line iY, and saves if needed buffer of the previous loaded
152 : // line (m_nLoadedLine).
153 : // Returns true if no error
154 2222 : bool GDALNearblackFloodFillAlg::LoadLine(int iY)
155 : {
156 2222 : if (iY != m_nLoadedLine)
157 : {
158 : #ifdef DEBUG
159 : // CPLDebug("GDAL", "GDALNearblackFloodFillAlg::LoadLine(%d)", iY);
160 : #endif
161 953 : const int nXSize = m_poSrcDataset->GetRasterXSize();
162 :
163 953 : if (m_nLoadedLine >= 0)
164 : {
165 1396 : if (m_bLineModified || (m_poDstDS != m_poSrcDataset &&
166 1396 : !m_abLineSavedOnce[m_nLoadedLine]))
167 : {
168 423 : if (m_poDstDS->RasterIO(
169 423 : GF_Write, 0, m_nLoadedLine, nXSize, 1, m_abyLine.data(),
170 423 : nXSize, 1, GDT_Byte, m_nDstBands, nullptr, m_nDstBands,
171 423 : static_cast<GSpacing>(nXSize) * m_nDstBands, 1,
172 423 : nullptr) != CE_None)
173 : {
174 0 : return false;
175 : }
176 : }
177 :
178 1198 : if (m_bSetMask &&
179 1198 : (m_bLineModified || !m_abLineSavedOnce[m_nLoadedLine]))
180 : {
181 118 : if (m_poMaskBand->RasterIO(GF_Write, 0, m_nLoadedLine, nXSize,
182 118 : 1, m_abyMask.data(), nXSize, 1,
183 118 : GDT_Byte, 0, 0, nullptr) != CE_None)
184 : {
185 0 : return false;
186 : }
187 : }
188 :
189 929 : m_abLineSavedOnce[m_nLoadedLine] = true;
190 : }
191 :
192 953 : if (iY >= 0)
193 : {
194 929 : if (m_poDstDS != m_poSrcDataset && m_abLineSavedOnce[iY])
195 : {
196 : // If the output dataset is different from the source one,
197 : // load from the output dataset if we have already written the
198 : // line of interest
199 497 : if (m_poDstDS->RasterIO(
200 497 : GF_Read, 0, iY, nXSize, 1, m_abyLine.data(), nXSize, 1,
201 497 : GDT_Byte, m_nDstBands, nullptr, m_nDstBands,
202 497 : static_cast<GSpacing>(nXSize) * m_nDstBands, 1,
203 497 : nullptr) != CE_None)
204 : {
205 0 : return false;
206 : }
207 : }
208 : else
209 : {
210 : // Otherwise load from the source data
211 432 : if (m_poSrcDataset->RasterIO(
212 432 : GF_Read, 0, iY, nXSize, 1, m_abyLine.data(), nXSize, 1,
213 : GDT_Byte,
214 : // m_nSrcBands intended
215 : m_nSrcBands,
216 : // m_nDstBands intended
217 432 : nullptr, m_nDstBands,
218 432 : static_cast<GSpacing>(nXSize) * m_nDstBands, 1,
219 432 : nullptr) != CE_None)
220 : {
221 0 : return false;
222 : }
223 :
224 : // Initialize the alpha component to 255 if it is the first time
225 : // we load that line.
226 432 : if (m_psOptions->bSetAlpha && !m_abLineLoadedOnce[iY])
227 : {
228 7650 : for (int iCol = 0; iCol < nXSize; iCol++)
229 : {
230 7500 : m_abyLine[iCol * m_nDstBands + m_nDstBands - 1] = 255;
231 : }
232 : }
233 : }
234 :
235 929 : if (m_bSetMask)
236 : {
237 269 : if (!m_abLineLoadedOnce[iY])
238 : {
239 2700 : for (int iCol = 0; iCol < nXSize; iCol++)
240 : {
241 2625 : m_abyMask[iCol] = 255;
242 : }
243 : }
244 : else
245 : {
246 194 : if (m_poMaskBand->RasterIO(
247 194 : GF_Read, 0, iY, nXSize, 1, m_abyMask.data(), nXSize,
248 194 : 1, GDT_Byte, 0, 0, nullptr) != CE_None)
249 : {
250 0 : return false;
251 : }
252 : }
253 : }
254 :
255 929 : if (!m_abLineLoadedOnce[iY])
256 : {
257 375 : m_nCountLoadedOnce++;
258 : // Very rough progression report based on the first time
259 : // we load a line...
260 : // We arbitrarily consider that it's 90% of the processing time
261 375 : const int nYSize = m_poSrcDataset->GetRasterYSize();
262 375 : if (!(m_psOptions->pfnProgress(
263 : 0.9 *
264 375 : (m_nCountLoadedOnce / static_cast<double>(nYSize)),
265 375 : nullptr, m_psOptions->pProgressData)))
266 : {
267 0 : return false;
268 : }
269 375 : m_abLineLoadedOnce[iY] = true;
270 : }
271 : }
272 :
273 953 : if (m_nLoadedLine >= 0)
274 : {
275 1858 : if (m_poVisitedDS->GetRasterBand(1)->RasterIO(
276 : GF_Write, 0, m_nLoadedLine, nXSize, 1,
277 929 : m_abyLineMustSet.data(), nXSize, 1, GDT_Byte, 0, 0,
278 929 : nullptr) != CE_None)
279 : {
280 0 : return false;
281 : }
282 : }
283 :
284 953 : if (iY >= 0)
285 : {
286 1858 : if (m_poVisitedDS->GetRasterBand(1)->RasterIO(
287 929 : GF_Read, 0, iY, nXSize, 1, m_abyLineMustSet.data(), nXSize,
288 929 : 1, GDT_Byte, 0, 0, nullptr) != CE_None)
289 : {
290 0 : return false;
291 : }
292 : }
293 :
294 953 : m_bLineModified = false;
295 953 : m_nLoadedLine = iY;
296 : }
297 2222 : return true;
298 : }
299 :
300 : /************************************************************************/
301 : /* GDALNearblackFloodFillAlg::Set() */
302 : /************************************************************************/
303 :
304 : // Mark the pixel as transparent
305 6633 : void GDALNearblackFloodFillAlg::Set(int iX, int iY)
306 : {
307 6633 : CPLAssert(iY == m_nLoadedLine);
308 6633 : CPL_IGNORE_RET_VAL(iY);
309 :
310 6633 : m_bLineModified = true;
311 6633 : m_abyLineMustSet[iX] = MUST_FILL_FALSE;
312 :
313 26182 : for (int iBand = 0; iBand < m_nSrcBands; iBand++)
314 19549 : m_abyLine[iX * m_nDstBands + iBand] = m_nReplacevalue;
315 :
316 : /***** alpha *****/
317 6633 : if (m_nDstBands > m_nSrcBands)
318 1946 : m_abyLine[iX * m_nDstBands + m_nDstBands - 1] = 0;
319 :
320 6633 : if (m_bSetMask)
321 879 : m_abyMask[iX] = 0;
322 6633 : }
323 :
324 : /************************************************************************/
325 : /* GDALNearblackFloodFillAlg::Fill() */
326 : /************************************************************************/
327 :
328 : /* Implements the "final, combined-scan-and-fill span filler was then published
329 : * in 1990" algorithm of https://en.wikipedia.org/wiki/Flood_fill#Span_filling
330 : * with the following enhancements:
331 : * - extra bound checking to avoid calling MustSet() outside the raster
332 : * - extra bound checking to avoid pushing spans outside the raster
333 : *
334 : * Returns true if no error.
335 : */
336 :
337 1652 : bool GDALNearblackFloodFillAlg::Fill(int iXInit, int iYInit)
338 : {
339 1652 : const int nXSize = m_poSrcDataset->GetRasterXSize();
340 1652 : const int nYSize = m_poSrcDataset->GetRasterYSize();
341 :
342 : struct Span
343 : {
344 : int x1;
345 : int x2;
346 : int y;
347 : int dy;
348 :
349 546 : Span(int x1In, int x2In, int yIn, int dyIn)
350 546 : : x1(x1In), x2(x2In), y(yIn), dy(dyIn)
351 : {
352 546 : }
353 : };
354 :
355 1652 : if (!LoadLine(iYInit))
356 0 : return false;
357 :
358 1652 : if (!MustSet(iXInit, iYInit))
359 : {
360 : // nothing to do
361 1632 : return true;
362 : }
363 :
364 40 : std::queue<Span> queue;
365 20 : queue.emplace(Span(iXInit, iXInit, iYInit, 1));
366 20 : if (iYInit > 0)
367 : {
368 7 : queue.emplace(Span(iXInit, iXInit, iYInit - 1, -1));
369 : }
370 :
371 566 : while (!queue.empty())
372 : {
373 : #ifdef DEBUG
374 546 : m_nMaxQueueSize = std::max(m_nMaxQueueSize, queue.size());
375 : #endif
376 :
377 546 : const Span s = queue.front();
378 546 : queue.pop();
379 :
380 546 : CPLAssert(s.x1 >= 0);
381 546 : CPLAssert(s.x1 < nXSize);
382 546 : CPLAssert(s.x2 >= 0);
383 546 : CPLAssert(s.x2 < nXSize);
384 546 : CPLAssert(s.x2 >= s.x1);
385 546 : CPLAssert(s.y >= 0);
386 546 : CPLAssert(s.y < nYSize);
387 :
388 546 : int iX = s.x1;
389 546 : const int iY = s.y;
390 :
391 546 : if (!LoadLine(iY))
392 0 : return false;
393 :
394 546 : if (iX > 0 && MustSet(iX, iY))
395 : {
396 56 : while (MustSet(iX - 1, iY))
397 : {
398 11 : Set(iX - 1, iY);
399 11 : iX--;
400 11 : if (iX == 0)
401 0 : break;
402 : }
403 : }
404 546 : if (iX >= 0 && iX <= s.x1 - 1 && iY - s.dy >= 0 && iY - s.dy < nYSize)
405 : {
406 8 : queue.emplace(Span(iX, s.x1 - 1, iY - s.dy, -s.dy));
407 : }
408 546 : int iX1 = s.x1;
409 546 : const int iX2 = s.x2;
410 1209 : while (iX1 <= iX2)
411 : {
412 7212 : while (MustSet(iX1, iY))
413 : {
414 6622 : Set(iX1, iY);
415 6622 : iX1++;
416 6622 : if (iX1 == nXSize)
417 73 : break;
418 : }
419 663 : if (iX <= iX1 - 1 && iY + s.dy >= 0 && iY + s.dy < nYSize)
420 : {
421 441 : queue.emplace(Span(iX, iX1 - 1, iY + s.dy, s.dy));
422 : }
423 663 : if (iX1 - 1 > iX2 && iY - s.dy >= 0 && iY - s.dy < nYSize)
424 : {
425 70 : queue.emplace(Span(iX2 + 1, iX1 - 1, iY - s.dy, -s.dy));
426 : }
427 663 : iX1++;
428 828 : while (iX1 < iX2 && !MustSet(iX1, iY))
429 165 : iX1++;
430 663 : iX = iX1;
431 : }
432 : }
433 :
434 20 : return true;
435 : }
436 :
437 : /************************************************************************/
438 : /* GDALNearblackFloodFillAlg::Process() */
439 : /************************************************************************/
440 :
441 : // Entry point.
442 : // Returns true if no error.
443 :
444 24 : bool GDALNearblackFloodFillAlg::Process()
445 : {
446 24 : const int nXSize = m_poSrcDataset->GetRasterXSize();
447 24 : const int nYSize = m_poSrcDataset->GetRasterYSize();
448 :
449 : /* -------------------------------------------------------------------- */
450 : /* Allocate working buffers. */
451 : /* -------------------------------------------------------------------- */
452 : try
453 : {
454 24 : m_abyLine.resize(static_cast<size_t>(nXSize) * m_nDstBands);
455 24 : m_abyLineMustSet.resize(nXSize);
456 24 : if (m_bSetMask)
457 18 : m_abyMask.resize(nXSize);
458 :
459 24 : if (m_psOptions->nMaxNonBlack > 0)
460 : {
461 12 : m_abLineLoadedOnce.resize(nYSize, true);
462 12 : m_abLineSavedOnce.resize(nYSize, true);
463 : }
464 : else
465 : {
466 12 : m_abLineLoadedOnce.resize(nYSize);
467 12 : m_abLineSavedOnce.resize(nYSize);
468 : }
469 : }
470 0 : catch (const std::exception &e)
471 : {
472 0 : CPLError(CE_Failure, CPLE_OutOfMemory,
473 0 : "Cannot allocate working buffers: %s", e.what());
474 0 : return false;
475 : }
476 :
477 : /* -------------------------------------------------------------------- */
478 : /* Create a temporary dataset to save visited state */
479 : /* -------------------------------------------------------------------- */
480 :
481 : // For debugging / testing purposes only
482 : const char *pszTmpDriver =
483 24 : CPLGetConfigOption("GDAL_TEMP_DRIVER_NAME", nullptr);
484 24 : if (!pszTmpDriver)
485 : {
486 24 : pszTmpDriver =
487 24 : (nXSize < 100 * 1024 * 1024 / nYSize ||
488 0 : (m_poDstDS->GetDriver() &&
489 0 : strcmp(m_poDstDS->GetDriver()->GetDescription(), "MEM") == 0))
490 24 : ? "MEM"
491 : : "GTiff";
492 : }
493 24 : GDALDriverH hDriver = GDALGetDriverByName(pszTmpDriver);
494 24 : if (!hDriver)
495 : {
496 0 : CPLError(CE_Failure, CPLE_AppDefined,
497 : "Cannot find driver %s for temporary file", pszTmpDriver);
498 0 : return false;
499 : }
500 48 : std::string osVisitedDataset = m_poDstDS->GetDescription();
501 : VSIStatBuf sStat;
502 48 : if (strcmp(pszTmpDriver, "MEM") == 0 ||
503 24 : STARTS_WITH(osVisitedDataset.c_str(), "/vsimem/") ||
504 : // Regular VSIStat() (not VSIStatL()) intended to check this is
505 : // a real file
506 0 : VSIStat(osVisitedDataset.c_str(), &sStat) == 0)
507 : {
508 24 : osVisitedDataset += ".visited";
509 : }
510 : else
511 : {
512 0 : osVisitedDataset = CPLGenerateTempFilename(osVisitedDataset.c_str());
513 : }
514 48 : CPLStringList aosOptions;
515 24 : if (strcmp(pszTmpDriver, "GTiff") == 0)
516 : {
517 0 : aosOptions.SetNameValue("SPARSE_OK", "YES");
518 0 : aosOptions.SetNameValue("COMPRESS", "LZW");
519 0 : osVisitedDataset += ".tif";
520 : }
521 24 : m_poVisitedDS.reset(GDALDataset::FromHandle(
522 : GDALCreate(hDriver, osVisitedDataset.c_str(), nXSize, nYSize, 1,
523 24 : GDT_Byte, aosOptions.List())));
524 24 : if (!m_poVisitedDS)
525 0 : return false;
526 24 : if (strcmp(pszTmpDriver, "MEM") != 0)
527 : {
528 0 : VSIUnlink(osVisitedDataset.c_str());
529 : }
530 24 : m_poVisitedDS->MarkSuppressOnClose();
531 :
532 : /* -------------------------------------------------------------------- */
533 : /* Iterate over the border of the raster */
534 : /* -------------------------------------------------------------------- */
535 : // Fill from top line
536 461 : for (int iX = 0; iX < nXSize; iX++)
537 : {
538 437 : if (!Fill(iX, 0))
539 0 : return false;
540 : }
541 :
542 : // Fill from left and right side
543 413 : for (int iY = 1; iY < nYSize - 1; iY++)
544 : {
545 389 : if (!Fill(0, iY))
546 0 : return false;
547 389 : if (!Fill(nXSize - 1, iY))
548 0 : return false;
549 : }
550 :
551 : // Fill from bottom line
552 461 : for (int iX = 0; iX < nXSize; iX++)
553 : {
554 437 : if (!Fill(iX, nYSize - 1))
555 0 : return false;
556 : }
557 :
558 24 : if (!(m_psOptions->pfnProgress(1.0, nullptr, m_psOptions->pProgressData)))
559 : {
560 0 : return false;
561 : }
562 :
563 : #ifdef DEBUG
564 24 : CPLDebug("GDAL", "flood fill max queue size = %u",
565 24 : unsigned(m_nMaxQueueSize));
566 : #endif
567 :
568 : // Force update of last visited line
569 24 : return LoadLine(-1);
570 : }
571 :
572 : /************************************************************************/
573 : /* GDALNearblackFloodFill() */
574 : /************************************************************************/
575 :
576 : // Entry point.
577 : // Returns true if no error.
578 :
579 24 : bool GDALNearblackFloodFill(const GDALNearblackOptions *psOptions,
580 : GDALDatasetH hSrcDataset, GDALDatasetH hDstDS,
581 : GDALRasterBandH hMaskBand, int nSrcBands,
582 : int nDstBands, bool bSetMask, const Colors &oColors)
583 : {
584 48 : GDALNearblackFloodFillAlg alg;
585 24 : alg.m_psOptions = psOptions;
586 24 : alg.m_poSrcDataset = GDALDataset::FromHandle(hSrcDataset);
587 24 : alg.m_poDstDS = GDALDataset::FromHandle(hDstDS);
588 24 : alg.m_poMaskBand = GDALRasterBand::FromHandle(hMaskBand);
589 24 : alg.m_nSrcBands = nSrcBands;
590 24 : alg.m_nDstBands = nDstBands;
591 24 : alg.m_bSetMask = bSetMask;
592 24 : alg.m_oColors = oColors;
593 24 : alg.m_nReplacevalue = psOptions->bNearWhite ? 255 : 0;
594 :
595 24 : if (psOptions->nMaxNonBlack > 0)
596 : {
597 : // First pass: use the TwoPasses algorithm to deal with nMaxNonBlack
598 24 : GDALNearblackOptions sOptionsTmp(*psOptions);
599 24 : sOptionsTmp.pProgressData = GDALCreateScaledProgress(
600 12 : 0, 0.5, psOptions->pfnProgress, psOptions->pProgressData);
601 12 : sOptionsTmp.pfnProgress = GDALScaledProgress;
602 12 : bool bRet = GDALNearblackTwoPassesAlgorithm(
603 : &sOptionsTmp, hSrcDataset, hDstDS, hMaskBand, nSrcBands, nDstBands,
604 : bSetMask, oColors);
605 12 : GDALDestroyScaledProgress(sOptionsTmp.pProgressData);
606 12 : if (!bRet)
607 0 : return false;
608 :
609 : // Second pass: use flood fill
610 24 : sOptionsTmp.pProgressData = GDALCreateScaledProgress(
611 12 : 0.5, 1, psOptions->pfnProgress, psOptions->pProgressData);
612 12 : sOptionsTmp.pfnProgress = GDALScaledProgress;
613 12 : alg.m_psOptions = &sOptionsTmp;
614 12 : bRet = alg.Process();
615 12 : GDALDestroyScaledProgress(sOptionsTmp.pProgressData);
616 12 : return bRet;
617 : }
618 : else
619 : {
620 12 : return alg.Process();
621 : }
622 : }
|