Coverage Report

Created: 2026-07-25 06:50

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/gdal/apps/gdalalg_mdim_compare.cpp
Line
Count
Source
1
/******************************************************************************
2
 *
3
 * Project:  GDAL
4
 * Purpose:  gdal "mdim compare" subcommand
5
 * Author:   Even Rouault <even dot rouault at spatialys.com>
6
 *
7
 ******************************************************************************
8
 * Copyright (c) 2026, Even Rouault <even dot rouault at spatialys.com>
9
 *
10
 * SPDX-License-Identifier: MIT
11
 ****************************************************************************/
12
13
#include "gdalalg_mdim_compare.h"
14
15
#include "gdal_dataset.h"
16
#include "gdal_multidim.h"
17
18
#include <algorithm>
19
#include <cinttypes>
20
#include <cmath>
21
#include <iterator>
22
#include <limits>
23
#include <set>
24
#include <utility>
25
26
//! @cond Doxygen_Suppress
27
28
#ifndef _
29
0
#define _(x) (x)
30
#endif
31
32
/************************************************************************/
33
/*         GDALMdimCompareAlgorithm::GDALMdimCompareAlgorithm()         */
34
/************************************************************************/
35
36
GDALMdimCompareAlgorithm::GDALMdimCompareAlgorithm(bool standaloneStep)
37
0
    : GDALMdimPipelineStepAlgorithm(NAME, DESCRIPTION, HELP_URL,
38
0
                                    ConstructorOptions()
39
0
                                        .SetStandaloneStep(standaloneStep)
40
0
                                        .SetInputDatasetMaxCount(1)
41
0
                                        .SetAddDefaultArguments(false))
42
0
{
43
0
    if (standaloneStep)
44
0
    {
45
0
        AddProgressArg();
46
0
    }
47
0
    else
48
0
    {
49
0
        AddMdimHiddenInputDatasetArg();
50
0
    }
51
52
0
    auto &referenceDatasetArg =
53
0
        AddArg("reference", 0, _("Reference dataset"), &m_referenceDataset,
54
0
               GDAL_OF_MULTIDIM_RASTER)
55
0
            .SetPositional()
56
0
            .SetRequired();
57
58
0
    SetAutoCompleteFunctionForFilename(referenceDatasetArg,
59
0
                                       GDAL_OF_MULTIDIM_RASTER);
60
61
0
    if (standaloneStep)
62
0
    {
63
0
        AddMdimInputArgs(/* openForMixedMdimVector = */ false,
64
0
                         /* hiddenForCLI = */ false,
65
0
                         /* acceptRaster = */ false);
66
0
    }
67
68
0
    AddArg("metric", 0, _("Comparison metric(s)"), &m_metrics)
69
0
        .SetChoices(METRIC_ALL, METRIC_NONE, METRIC_DIFF, METRIC_RMSD,
70
0
                    METRIC_PSNR)
71
0
        .SetDefault(METRIC_DEFAULT);
72
73
0
    AddArrayNameArg(&m_array, _("Name of array(s) to compare"));
74
75
0
    AddOutputStringArg(&m_output);
76
77
0
    AddArg("skip-binary", 0, _("Skip binary file comparison"), &m_skipBinary);
78
79
0
    AddArg("return-code", 0, _("Return code"), &m_retCode)
80
0
        .SetHiddenForCLI()
81
0
        .SetIsInput(false)
82
0
        .SetIsOutput(true);
83
0
}
84
85
/************************************************************************/
86
/*                           GetPixelCount()                            */
87
/************************************************************************/
88
89
static uint64_t GetPixelCount(const std::shared_ptr<GDALMDArray> &array)
90
0
{
91
0
    uint64_t nPixels = 1;
92
0
    for (const auto &poDim : array->GetDimensions())
93
0
    {
94
0
        nPixels *= poDim->GetSize();
95
0
    }
96
0
    return nPixels;
97
0
}
98
99
/************************************************************************/
100
/*                 GDALMdimCompareAlgorithm::RunStep()                  */
101
/************************************************************************/
102
103
bool GDALMdimCompareAlgorithm::RunStep(GDALPipelineStepRunContext &ctxt)
104
0
{
105
0
    auto poRefDS = m_referenceDataset.GetDatasetRef();
106
0
    CPLAssert(poRefDS);
107
108
0
    CPLAssert(m_inputDataset.size() == 1);
109
0
    auto poInputDS = m_inputDataset[0].GetDatasetRef();
110
0
    CPLAssert(poInputDS);
111
112
0
    std::vector<std::string> aosReport;
113
114
0
    if (!m_skipBinary)
115
0
    {
116
0
        if (BinaryComparison(this, aosReport, poRefDS, poInputDS))
117
0
        {
118
0
            return true;
119
0
        }
120
0
    }
121
122
0
    auto poRefRootGroup = poRefDS->GetRootGroup();
123
0
    if (!poRefRootGroup)
124
0
    {
125
0
        ReportError(CE_Failure, CPLE_AppDefined,
126
0
                    "Cannot get root group on reference dataset");
127
0
        return false;
128
0
    }
129
130
0
    auto poInputRootGroup = poInputDS->GetRootGroup();
131
0
    if (!poInputRootGroup)
132
0
    {
133
0
        ReportError(CE_Failure, CPLE_AppDefined,
134
0
                    "Cannot get root group on input dataset");
135
0
        return false;
136
0
    }
137
138
0
    auto refArrays = poRefRootGroup->GetMDArrayFullNamesRecursive();
139
0
    auto inputArrays = poInputRootGroup->GetMDArrayFullNamesRecursive();
140
0
    if (m_array.empty())
141
0
    {
142
0
        std::sort(refArrays.begin(), refArrays.end());
143
0
        std::sort(inputArrays.begin(), inputArrays.end());
144
145
0
        {
146
0
            std::vector<std::string> missing;
147
0
            std::set_difference(refArrays.begin(), refArrays.end(),
148
0
                                inputArrays.begin(), inputArrays.end(),
149
0
                                std::back_inserter(missing));
150
0
            if (!missing.empty())
151
0
            {
152
0
                std::string line =
153
0
                    "The following arrays are found in the reference dataset, "
154
0
                    "but missing in the input one: ";
155
0
                bool first = true;
156
0
                for (const auto &name : missing)
157
0
                {
158
0
                    if (!first)
159
0
                        line += ", ";
160
0
                    first = false;
161
0
                    line += name;
162
0
                }
163
0
                aosReport.push_back(std::move(line));
164
0
            }
165
0
        }
166
167
0
        {
168
0
            std::vector<std::string> missing;
169
0
            std::set_difference(inputArrays.begin(), inputArrays.end(),
170
0
                                refArrays.begin(), refArrays.end(),
171
0
                                std::back_inserter(missing));
172
0
            if (!missing.empty())
173
0
            {
174
0
                std::string line =
175
0
                    "The following arrays are found in the input dataset, but "
176
0
                    "missing in the reference one: ";
177
0
                bool first = true;
178
0
                for (const auto &name : missing)
179
0
                {
180
0
                    if (!first)
181
0
                        line += ", ";
182
0
                    first = false;
183
0
                    line += name;
184
0
                }
185
0
                aosReport.push_back(std::move(line));
186
0
            }
187
0
        }
188
189
0
        std::set_intersection(refArrays.begin(), refArrays.end(),
190
0
                              inputArrays.begin(), inputArrays.end(),
191
0
                              std::back_inserter(m_array));
192
0
    }
193
0
    else
194
0
    {
195
0
        std::set<std::string> newArrays;
196
0
        for (const auto &name : m_array)
197
0
        {
198
0
            const auto ExistsIn =
199
0
                [&name,
200
0
                 &newArrays](const std::vector<std::string> &aosExitingArrays,
201
0
                             bool insert)
202
0
            {
203
0
                bool ret = false;
204
0
                for (const auto &exitingArray : aosExitingArrays)
205
0
                {
206
0
                    if (!name.empty() &&
207
0
                        ((name[0] == '/' && name == exitingArray) ||
208
0
                         (name[0] != '/' &&
209
0
                          name == CPLGetFilename(exitingArray.c_str()))))
210
0
                    {
211
0
                        if (insert)
212
0
                            newArrays.insert(exitingArray);
213
0
                        ret = true;
214
0
                    }
215
0
                }
216
0
                return ret;
217
0
            };
218
219
0
            if (!ExistsIn(refArrays, true))
220
0
            {
221
0
                ReportError(CE_Failure, CPLE_AppDefined,
222
0
                            "Array '%s' does not exist in reference dataset",
223
0
                            name.c_str());
224
0
                return false;
225
0
            }
226
0
            if (!ExistsIn(inputArrays, false))
227
0
            {
228
0
                ReportError(CE_Failure, CPLE_AppDefined,
229
0
                            "Array '%s' does not exist in input dataset",
230
0
                            name.c_str());
231
0
                return false;
232
0
            }
233
0
        }
234
0
        m_array.clear();
235
0
        m_array.insert(m_array.end(), newArrays.begin(), newArrays.end());
236
0
    }
237
238
0
    std::vector<
239
0
        std::pair<std::shared_ptr<GDALMDArray>, std::shared_ptr<GDALMDArray>>>
240
0
        arrayPairs;
241
    // not zero, to avoid false positive Coverity Scan warning
242
0
    double dfTotalPixels = std::numeric_limits<double>::min();
243
0
    for (const auto &array : m_array)
244
0
    {
245
0
        auto poRefArray = poRefRootGroup->OpenMDArrayFromFullname(array);
246
0
        if (!poRefArray)
247
0
        {
248
0
            ReportError(CE_Failure, CPLE_AppDefined,
249
0
                        "Array '%s' cannot be opened in reference dataset",
250
0
                        array.c_str());
251
0
            return false;
252
0
        }
253
254
0
        auto poInputArray = poInputRootGroup->OpenMDArrayFromFullname(array);
255
0
        if (!poInputArray)
256
0
        {
257
0
            ReportError(CE_Failure, CPLE_AppDefined,
258
0
                        "Array '%s' cannot be opened in input dataset",
259
0
                        array.c_str());
260
0
            return false;
261
0
        }
262
263
0
        dfTotalPixels += static_cast<double>(GetPixelCount(poRefArray));
264
0
        arrayPairs.emplace_back(std::move(poRefArray), std::move(poInputArray));
265
0
    }
266
267
0
    uint64_t nCurPixels = 0;
268
0
    for (const auto &[poRefArray, poInputArray] : arrayPairs)
269
0
    {
270
0
        const uint64_t nThisArrayPixels = GetPixelCount(poRefArray);
271
0
        const double dfMinPct = static_cast<double>(nCurPixels) / dfTotalPixels;
272
0
        const double dfMaxPct =
273
0
            static_cast<double>(nCurPixels + nThisArrayPixels) / dfTotalPixels;
274
0
        std::unique_ptr<void, decltype(&GDALDestroyScaledProgress)>
275
0
            pScaledProgress(GDALCreateScaledProgress(dfMinPct, dfMaxPct,
276
0
                                                     ctxt.m_pfnProgress,
277
0
                                                     ctxt.m_pProgressData),
278
0
                            GDALDestroyScaledProgress);
279
0
        CompareArray(aosReport, poRefArray, poInputArray,
280
0
                     pScaledProgress ? GDALScaledProgress : nullptr,
281
0
                     pScaledProgress.get());
282
0
        nCurPixels += nThisArrayPixels;
283
0
    }
284
285
0
    if (ctxt.m_pfnProgress)
286
0
        ctxt.m_pfnProgress(1.0, "", ctxt.m_pProgressData);
287
288
0
    for (const auto &s : aosReport)
289
0
    {
290
0
        m_output += s;
291
0
        m_output += '\n';
292
0
    }
293
294
0
    m_retCode = static_cast<int>(aosReport.size());
295
296
0
    return true;
297
0
}
298
299
/************************************************************************/
300
/*                         NonZeroValueIterator                         */
301
/************************************************************************/
302
303
namespace
304
{
305
struct NonZeroValueIterator
306
{
307
    std::vector<double> adfValues{};
308
    uint64_t nNonZero = 0;
309
310
    static bool func(GDALAbstractMDArray *array,
311
                     const GUInt64 *chunkArrayStartIdx,
312
                     const size_t *chunkCount, GUInt64 /* iCurChunk */,
313
                     GUInt64 /* nChunkCount */, void *pUserData)
314
0
    {
315
0
        auto self = static_cast<NonZeroValueIterator *>(pUserData);
316
0
        const size_t nDims = array->GetDimensionCount();
317
0
        size_t nElts = 1;
318
0
        for (size_t i = 0; i < nDims; ++i)
319
0
            nElts *= chunkCount[i];
320
0
        if (self->adfValues.size() < nElts)
321
0
        {
322
0
            try
323
0
            {
324
0
                self->adfValues.resize(nElts);
325
0
            }
326
0
            catch (const std::exception &)
327
0
            {
328
0
                CPLError(CE_Failure, CPLE_OutOfMemory, "Out of memory");
329
0
                return false;
330
0
            }
331
0
        }
332
0
        if (!array->Read(chunkArrayStartIdx, chunkCount,
333
0
                         /* step = */ nullptr, /* stride = */ nullptr,
334
0
                         GDALExtendedDataType::Create(GDT_Float64),
335
0
                         self->adfValues.data()))
336
0
        {
337
0
            return false;
338
0
        }
339
0
        for (size_t i = 0; i < nElts; ++i)
340
0
        {
341
0
            if (self->adfValues[i] != 0)
342
0
                self->nNonZero++;
343
0
        }
344
345
0
        return true;
346
0
    }
347
};
348
}  // namespace
349
350
/************************************************************************/
351
/*                          GetDataTypeName()                           */
352
/************************************************************************/
353
354
static std::string GetDataTypeName(const GDALExtendedDataType &dt)
355
0
{
356
0
    switch (dt.GetClass())
357
0
    {
358
0
        case GEDTC_NUMERIC:
359
0
            return GDALGetDataTypeName(dt.GetNumericDataType());
360
0
        case GEDTC_STRING:
361
0
            return "String";
362
0
        case GEDTC_COMPOUND:
363
0
            break;
364
0
    }
365
0
    return "Compound";
366
0
}
367
368
/************************************************************************/
369
/*               GDALMdimCompareAlgorithm::CompareArray()               */
370
/************************************************************************/
371
372
void GDALMdimCompareAlgorithm::CompareArray(
373
    std::vector<std::string> &aosReport,
374
    const std::shared_ptr<GDALMDArray> &poRefArray,
375
    const std::shared_ptr<GDALMDArray> &poInputArray,
376
    GDALProgressFunc pfnProgress, void *pProgressData)
377
0
{
378
0
    const auto &osName = poRefArray->GetFullName();
379
0
    const auto nDims = poRefArray->GetDimensionCount();
380
0
    if (nDims != poInputArray->GetDimensionCount())
381
0
    {
382
0
        aosReport.push_back(
383
0
            CPLSPrintf("Array %s: dimension count in reference is %d, whereas "
384
0
                       "it is %d in input",
385
0
                       osName.c_str(), static_cast<int>(nDims),
386
0
                       static_cast<int>(poInputArray->GetDimensionCount())));
387
0
        return;
388
0
    }
389
390
0
    if (!poRefArray->HasSameShapeAs(*poInputArray))
391
0
    {
392
0
        const auto ShapeToString = [](const std::shared_ptr<GDALMDArray> &array)
393
0
        {
394
0
            std::string s("[");
395
0
            for (const auto &poDim : array->GetDimensions())
396
0
            {
397
0
                if (s.size() > 1)
398
0
                    s += ',';
399
0
                s += std::to_string(poDim->GetSize());
400
0
            }
401
0
            s += ']';
402
0
            return s;
403
0
        };
404
405
0
        aosReport.push_back(CPLSPrintf(
406
0
            "Array %s: shape in reference is %s, whereas it is %s in input",
407
0
            osName.c_str(), ShapeToString(poRefArray).c_str(),
408
0
            ShapeToString(poInputArray).c_str()));
409
0
        return;
410
0
    }
411
412
0
    if (poRefArray->GetDataType() != poInputArray->GetDataType())
413
0
    {
414
0
        aosReport.push_back(CPLSPrintf(
415
0
            "Array %s: data type in reference is %s, whereas it is %s in input",
416
0
            osName.c_str(), GetDataTypeName(poRefArray->GetDataType()).c_str(),
417
0
            GetDataTypeName(poInputArray->GetDataType()).c_str()));
418
0
    }
419
0
    if (poRefArray->GetDataType().GetClass() != GEDTC_NUMERIC ||
420
0
        poInputArray->GetDataType().GetClass() != GEDTC_NUMERIC)
421
0
    {
422
0
        CPLError(CE_Warning, CPLE_AppDefined,
423
0
                 "Array %s: not compared, as comparison of non-numeric data "
424
0
                 "types is not currently supported",
425
0
                 osName.c_str());
426
0
        return;
427
0
    }
428
429
0
    auto diffArray = (*poRefArray) - poInputArray;
430
0
    if (!diffArray)
431
0
    {
432
        // Given above checks, this shouldn't happen.
433
0
        aosReport.push_back(CPLSPrintf(
434
0
            "Array %s: cannot compute array of differences", osName.c_str()));
435
0
        return;
436
0
    }
437
438
0
    int nCountMetrics = 0;
439
0
    if (HasMetric(METRIC_DIFF))
440
0
        ++nCountMetrics;
441
0
    if (HasMetric(METRIC_RMSD) || HasMetric(METRIC_PSNR))
442
0
        ++nCountMetrics;
443
444
0
    double dfLastPct = 0;
445
0
    if (HasMetric(METRIC_DIFF))
446
0
    {
447
0
        double dfMin = 0, dfMax = 0;
448
449
0
        const double dfNewLastPct = 1.0 / nCountMetrics;
450
0
        std::unique_ptr<void, decltype(&GDALDestroyScaledProgress)>
451
0
            pScaledProgress(GDALCreateScaledProgress(
452
0
                                0.0, dfNewLastPct, pfnProgress, pProgressData),
453
0
                            GDALDestroyScaledProgress);
454
0
        dfLastPct = dfNewLastPct;
455
456
0
        if (!diffArray->ComputeStatistics(
457
0
                /* bApproxOK =*/false, &dfMin, &dfMax, nullptr, nullptr,
458
0
                nullptr, pScaledProgress ? GDALScaledProgress : nullptr,
459
0
                pScaledProgress.get(), nullptr))
460
0
        {
461
0
            aosReport.push_back(CPLSPrintf(
462
0
                "Array %s: cannot compute statistics", osName.c_str()));
463
0
            return;
464
0
        }
465
0
        if (dfMin != 0 || dfMax != 0)
466
0
        {
467
0
            const double dfMaxDiff =
468
0
                std::max(std::fabs(dfMin), std::fabs(dfMax));
469
0
            aosReport.push_back(
470
0
                CPLSPrintf("Array %s: maximum pixel value difference: %g",
471
0
                           osName.c_str(), dfMaxDiff));
472
473
0
            NonZeroValueIterator it;
474
0
            std::vector<GUInt64> arrayStartIdx(nDims, 0);
475
0
            std::vector<GUInt64> count(nDims, 0);
476
0
            for (size_t i = 0; i < nDims; ++i)
477
0
                count[i] = diffArray->GetDimensions()[i]->GetSize();
478
479
0
            size_t nMaxSize = 100 * 1024 * 1024;
480
0
            const GIntBig nUsableRAM = CPLGetUsablePhysicalRAM() / 10;
481
0
            if (nUsableRAM > 0)
482
0
                nMaxSize = static_cast<size_t>(nUsableRAM);
483
484
0
            if (!diffArray->ProcessPerChunk(
485
0
                    arrayStartIdx.data(), count.data(),
486
0
                    diffArray->GetProcessingChunkSize(nMaxSize).data(),
487
0
                    NonZeroValueIterator::func, &it))
488
0
            {
489
0
                aosReport.push_back(
490
0
                    CPLSPrintf("Array %s: diffArray->ProcessPerChunk() failed",
491
0
                               osName.c_str()));
492
0
                return;
493
0
            }
494
495
0
            aosReport.push_back(
496
0
                CPLSPrintf("Array %s: pixels differing: %" PRIu64,
497
0
                           osName.c_str(), it.nNonZero));
498
0
        }
499
0
    }
500
501
0
    if (HasMetric(METRIC_RMSD) || HasMetric(METRIC_PSNR))
502
0
    {
503
        // For PSNR on floating point image, we need to compute min and max of
504
        // reference band
505
0
        const bool bIsInteger = CPL_TO_BOOL(GDALDataTypeIsInteger(
506
0
            poRefArray->GetDataType().GetNumericDataType()));
507
0
        const double dfScalingProgress =
508
0
            HasMetric(METRIC_PSNR) && !bIsInteger ? 0.5 : 1;
509
0
        const double dfNewLastPct =
510
0
            std::min(1.0, dfLastPct + dfScalingProgress * (1.0 - dfLastPct));
511
0
        std::unique_ptr<void, decltype(&GDALDestroyScaledProgress)>
512
0
            pScaledProgress(GDALCreateScaledProgress(dfLastPct, dfNewLastPct,
513
0
                                                     pfnProgress,
514
0
                                                     pProgressData),
515
0
                            GDALDestroyScaledProgress);
516
0
        dfLastPct = dfNewLastPct;
517
518
0
        auto squaredDiffArray = (*diffArray) * diffArray;
519
0
        double dfMeanSquareError = 0;
520
0
        if (squaredDiffArray->ComputeStatistics(
521
0
                /* bApproxOK = */ false,
522
0
                /* pdfMin = */ nullptr,
523
0
                /* pdfMax = */ nullptr, &dfMeanSquareError,
524
0
                /* pdfStdDev = */ nullptr, nullptr,
525
0
                pScaledProgress ? GDALScaledProgress : nullptr,
526
0
                pScaledProgress.get(), nullptr))
527
0
        {
528
0
            const double dfRMSD = std::sqrt(dfMeanSquareError);
529
0
            if (dfRMSD > 0)
530
0
            {
531
0
                if (HasMetric(METRIC_RMSD))
532
0
                {
533
0
                    aosReport.push_back(CPLSPrintf("Array %s: RMSD: %g",
534
0
                                                   osName.c_str(), dfRMSD));
535
0
                }
536
537
0
                if (HasMetric(METRIC_PSNR))
538
0
                {
539
0
                    if (bIsInteger)
540
0
                    {
541
0
                        double dfMaxAmplitude =
542
0
                            std::pow(2.0, GDALGetDataTypeSizeBits(
543
0
                                              poRefArray->GetDataType()
544
0
                                                  .GetNumericDataType())) -
545
0
                            1;
546
547
0
                        const double dfPSNR_dB =
548
0
                            20 * std::log10(dfMaxAmplitude / dfRMSD);
549
0
                        aosReport.push_back(
550
0
                            CPLSPrintf("Array %s: PSNR (dB): %g",
551
0
                                       osName.c_str(), dfPSNR_dB));
552
0
                    }
553
0
                    else
554
0
                    {
555
0
                        double dfMin = 0;
556
0
                        double dfMax = 0;
557
0
                        const char *const apszOptions[] = {
558
0
                            "SET_STATISTICS=FALSE", nullptr};
559
560
0
                        pScaledProgress.reset(GDALCreateScaledProgress(
561
0
                            dfLastPct, 1.0, pfnProgress, pProgressData));
562
563
0
                        if (poRefArray->ComputeStatistics(
564
0
                                /* bApproxOK = */ false, &dfMin, &dfMax,
565
0
                                nullptr, nullptr, nullptr,
566
0
                                pScaledProgress ? GDALScaledProgress : nullptr,
567
0
                                pScaledProgress.get(), apszOptions))
568
0
                        {
569
0
                            const double dfPSNR_dB =
570
0
                                20 * std::log10((dfMax - dfMin) / dfRMSD);
571
0
                            aosReport.push_back(
572
0
                                CPLSPrintf("Array %s: PSNR (dB): %g",
573
0
                                           osName.c_str(), dfPSNR_dB));
574
0
                        }
575
0
                        else
576
0
                        {
577
0
                            aosReport.push_back(
578
0
                                std::string("Error during PSNR computation: ")
579
0
                                    .append(CPLGetLastErrorMsg()));
580
0
                        }
581
0
                    }
582
0
                }
583
0
            }
584
0
        }
585
0
        else
586
0
        {
587
0
            aosReport.push_back(
588
0
                std::string("Error during RMSD/PSNR computation: ")
589
0
                    .append(CPLGetLastErrorMsg()));
590
0
        }
591
0
    }
592
0
}
593
594
/************************************************************************/
595
/*                ~GDALMdimCompareAlgorithmStandalone()                 */
596
/************************************************************************/
597
598
0
GDALMdimCompareAlgorithmStandalone::~GDALMdimCompareAlgorithmStandalone() =
599
    default;
600
601
//! @endcond