Coverage Report

Created: 2026-07-25 06:50

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/gdal/apps/gdalalg_raster_neighbors.cpp
Line
Count
Source
1
/******************************************************************************
2
 *
3
 * Project:  GDAL
4
 * Purpose:  "neighbors" step of "raster pipeline"
5
 * Author:   Even Rouault <even dot rouault at spatialys.com>
6
 *
7
 ******************************************************************************
8
 * Copyright (c) 2025, Even Rouault <even dot rouault at spatialys.com>
9
 *
10
 * SPDX-License-Identifier: MIT
11
 ****************************************************************************/
12
13
#include "gdalalg_raster_neighbors.h"
14
15
#include "gdal_priv.h"
16
#include "gdal_priv_templates.hpp"
17
#include "vrtdataset.h"
18
19
#include <algorithm>
20
#include <limits>
21
#include <map>
22
#include <optional>
23
#include <set>
24
#include <utility>
25
#include <vector>
26
27
//! @cond Doxygen_Suppress
28
29
#ifndef _
30
0
#define _(x) (x)
31
#endif
32
33
static const std::set<std::string> oSetKernelNames = {
34
    "u",     "v",       "equal",    "edge1",
35
    "edge2", "sharpen", "gaussian", "unsharp-masking"};
36
37
namespace
38
{
39
struct KernelDef
40
{
41
    int size = 0;
42
    std::vector<double> adfCoefficients{};
43
};
44
}  // namespace
45
46
// clang-format off
47
// Cf https://en.wikipedia.org/wiki/Kernel_(image_processing)
48
static const std::map<std::string, std::pair<int, std::vector<int>>> oMapKernelNameToMatrix = {
49
    { "u", { 3, {  0,  0,  0,
50
                  -1,  0,  1,
51
                   0,  0,  0 } } },
52
    { "v", { 3, {  0, -1,  0,
53
                   0,  0,  0,
54
                   0,  1,  0 } } },
55
    { "edge1", { 3, {  0, -1,  0,
56
                      -1,  4, -1,
57
                       0, -1,  0 } } },
58
    { "edge2", { 3, { -1, -1, -1,
59
                      -1,  8, -1,
60
                      -1, -1, -1 } } },
61
    { "sharpen",  { 3, { 0, -1,  0,
62
                        -1,  5, -1,
63
                         0, -1,  0 } } },
64
    { "gaussian-3x3", { 3, { 1, 2, 1,
65
                                  2, 4, 2,
66
                                  1, 2, 1 } } },
67
    { "gaussian-5x5", { 5, { 1, 4,   6,  4, 1,
68
                                  4, 16, 24, 16, 4,
69
                                  6, 24, 36, 24, 6,
70
                                  4, 16, 24, 16, 4,
71
                                  1, 4,   6,  4, 1, } } },
72
    { "unsharp-masking-5x5", { 5, { 1, 4,     6,  4, 1,
73
                                    4, 16,   24, 16, 4,
74
                                    6, 24, -476, 24, 6,
75
                                    4, 16,   24, 16, 4,
76
                                    1, 4,     6,  4, 1, } } },
77
};
78
79
// clang-format on
80
81
/************************************************************************/
82
/*                        CreateDerivedBandXML()                        */
83
/************************************************************************/
84
85
static bool CreateDerivedBandXML(VRTDataset *poVRTDS, GDALRasterBand *poSrcBand,
86
                                 GDALDataType eType, const std::string &noData,
87
                                 const std::string &method,
88
                                 const KernelDef &kernelDef)
89
0
{
90
0
    poVRTDS->AddBand(eType, nullptr);
91
92
0
    std::optional<double> dstNoData;
93
0
    bool autoSelectNoDataValue = false;
94
0
    if (noData.empty())
95
0
    {
96
0
        autoSelectNoDataValue = true;
97
0
    }
98
0
    else if (noData != "none")
99
0
    {
100
        // Already validated to be numeric by the validation action
101
0
        dstNoData = CPLAtof(noData.c_str());
102
0
    }
103
104
0
    auto poVRTBand = cpl::down_cast<VRTSourcedRasterBand *>(
105
0
        poVRTDS->GetRasterBand(poVRTDS->GetRasterCount()));
106
107
0
    auto poSource = std::make_unique<VRTKernelFilteredSource>();
108
0
    poSrcBand->GetDataset()->Reference();
109
0
    poSource->SetSrcBand(poSrcBand);
110
0
    poSource->SetKernel(kernelDef.size, /* separable = */ false,
111
0
                        kernelDef.adfCoefficients);
112
0
    poSource->SetNormalized(method != "sum");
113
0
    if (method != "sum" && method != "mean")
114
0
        poSource->SetFunction(method.c_str());
115
116
0
    int bSrcHasNoData = false;
117
0
    const double dfNoDataValue = poSrcBand->GetNoDataValue(&bSrcHasNoData);
118
0
    if (bSrcHasNoData)
119
0
    {
120
0
        poSource->SetNoDataValue(dfNoDataValue);
121
0
        if (autoSelectNoDataValue && !dstNoData.has_value())
122
0
        {
123
0
            dstNoData = dfNoDataValue;
124
0
        }
125
0
    }
126
127
0
    if (dstNoData.has_value())
128
0
    {
129
0
        if (!GDALIsValueExactAs(dstNoData.value(), eType))
130
0
        {
131
0
            CPLError(CE_Failure, CPLE_AppDefined,
132
0
                     "Band output type %s cannot represent NoData value %g",
133
0
                     GDALGetDataTypeName(eType), dstNoData.value());
134
0
            return false;
135
0
        }
136
137
0
        poVRTBand->SetNoDataValue(dstNoData.value());
138
0
    }
139
140
0
    poVRTBand->AddSource(std::move(poSource));
141
142
0
    return true;
143
0
}
144
145
/************************************************************************/
146
/*                   GDALNeighborsCreateVRTDerived()                    */
147
/************************************************************************/
148
149
static std::unique_ptr<GDALDataset>
150
GDALNeighborsCreateVRTDerived(GDALDataset *poSrcDS, int nBand,
151
                              GDALDataType eType, const std::string &noData,
152
                              const std::vector<std::string> &methods,
153
                              const std::vector<KernelDef> &aKernelDefs)
154
0
{
155
0
    CPLAssert(methods.size() == aKernelDefs.size());
156
157
0
    auto ds = std::make_unique<VRTDataset>(poSrcDS->GetRasterXSize(),
158
0
                                           poSrcDS->GetRasterYSize());
159
0
    GDALGeoTransform gt;
160
0
    if (poSrcDS->GetGeoTransform(gt) == CE_None)
161
0
        ds->SetGeoTransform(gt);
162
0
    if (const OGRSpatialReference *poSRS = poSrcDS->GetSpatialRef())
163
0
    {
164
0
        ds->SetSpatialRef(poSRS);
165
0
    }
166
167
0
    bool ret = true;
168
0
    if (nBand != 0)
169
0
    {
170
0
        for (size_t i = 0; i < aKernelDefs.size() && ret; ++i)
171
0
        {
172
0
            ret =
173
0
                CreateDerivedBandXML(ds.get(), poSrcDS->GetRasterBand(nBand),
174
0
                                     eType, noData, methods[i], aKernelDefs[i]);
175
0
        }
176
0
    }
177
0
    else
178
0
    {
179
0
        for (int iBand = 1; iBand <= poSrcDS->GetRasterCount(); ++iBand)
180
0
        {
181
0
            for (size_t i = 0; i < aKernelDefs.size() && ret; ++i)
182
0
            {
183
0
                ret = CreateDerivedBandXML(ds.get(),
184
0
                                           poSrcDS->GetRasterBand(iBand), eType,
185
0
                                           noData, methods[i], aKernelDefs[i]);
186
0
            }
187
0
        }
188
0
    }
189
0
    if (!ret)
190
0
        ds.reset();
191
0
    return ds;
192
0
}
193
194
/************************************************************************/
195
/*     GDALRasterNeighborsAlgorithm::GDALRasterNeighborsAlgorithm()     */
196
/************************************************************************/
197
198
GDALRasterNeighborsAlgorithm::GDALRasterNeighborsAlgorithm(
199
    bool standaloneStep) noexcept
200
0
    : GDALRasterPipelineStepAlgorithm(
201
0
          NAME, DESCRIPTION, HELP_URL,
202
0
          ConstructorOptions().SetStandaloneStep(standaloneStep))
203
0
{
204
0
    AddBandArg(&m_band);
205
206
0
    AddArg("method", 0, _("Method to combine weighed source pixels"), &m_method)
207
0
        .SetChoices("mean", "sum", "min", "max", "stddev", "median", "mode");
208
209
0
    AddArg("size", 0, _("Neighborhood size"), &m_size)
210
0
        .SetMinValueIncluded(3)
211
0
        .SetMaxValueIncluded(99)
212
0
        .AddValidationAction(
213
0
            [this]()
214
0
            {
215
0
                if ((m_size % 2) != 1)
216
0
                {
217
0
                    ReportError(CE_Failure, CPLE_IllegalArg,
218
0
                                "The value of 'size' must be an odd number.");
219
0
                    return false;
220
0
                }
221
0
                return true;
222
0
            });
223
224
0
    AddArg("kernel", 0, _("Convolution kernel(s) to apply"), &m_kernel)
225
0
        .SetPackedValuesAllowed(false)
226
0
        .SetMinCount(1)
227
0
        .SetMinCharCount(1)
228
0
        .SetRequired()
229
0
        .SetAutoCompleteFunction(
230
0
            [](const std::string &v)
231
0
            {
232
0
                std::vector<std::string> ret;
233
0
                if (v.empty() || v.front() != '[')
234
0
                {
235
0
                    ret.insert(ret.end(), oSetKernelNames.begin(),
236
0
                               oSetKernelNames.end());
237
0
                    ret.push_back(
238
0
                        "[[val00,val10,...,valN0],...,[val0N,val1N,...valNN]]");
239
0
                }
240
0
                return ret;
241
0
            })
242
0
        .AddValidationAction(
243
0
            [this]()
244
0
            {
245
0
                for (const std::string &kernel : m_kernel)
246
0
                {
247
0
                    if (kernel.front() == '[' && kernel.back() == ']')
248
0
                    {
249
0
                        const CPLStringList aosValues(CSLTokenizeString2(
250
0
                            kernel.c_str(), "[] ,",
251
0
                            CSLT_STRIPLEADSPACES | CSLT_STRIPENDSPACES));
252
0
                        const double dfSize =
253
0
                            static_cast<double>(aosValues.size());
254
0
                        const int nSqrt = static_cast<int>(sqrt(dfSize) + 0.5);
255
0
                        if (!((aosValues.size() % 2) == 1 &&
256
0
                              nSqrt * nSqrt == aosValues.size()))
257
0
                        {
258
0
                            ReportError(
259
0
                                CE_Failure, CPLE_IllegalArg,
260
0
                                "The number of values in the 'kernel' "
261
0
                                "argument must be an odd square number.");
262
0
                            return false;
263
0
                        }
264
0
                        for (int i = 0; i < aosValues.size(); ++i)
265
0
                        {
266
0
                            if (CPLGetValueType(aosValues[i]) ==
267
0
                                CPL_VALUE_STRING)
268
0
                            {
269
0
                                ReportError(CE_Failure, CPLE_IllegalArg,
270
0
                                            "Non-numeric value found in the "
271
0
                                            "'kernel' argument");
272
0
                                return false;
273
0
                            }
274
0
                        }
275
0
                    }
276
0
                    else if (!cpl::contains(oSetKernelNames, kernel))
277
0
                    {
278
0
                        std::string osMsg =
279
0
                            "Valid values for 'kernel' argument are: ";
280
0
                        bool bFirst = true;
281
0
                        for (const auto &name : oSetKernelNames)
282
0
                        {
283
0
                            if (!bFirst)
284
0
                                osMsg += ", ";
285
0
                            bFirst = false;
286
0
                            osMsg += '\'';
287
0
                            osMsg += name;
288
0
                            osMsg += '\'';
289
0
                        }
290
0
                        osMsg += " or "
291
0
                                 "[[val00,val10,...,valN0],...,[val0N,val1N,..."
292
0
                                 "valNN]]";
293
0
                        ReportError(CE_Failure, CPLE_IllegalArg, "%s",
294
0
                                    osMsg.c_str());
295
0
                        return false;
296
0
                    }
297
0
                }
298
0
                return true;
299
0
            });
300
301
0
    AddOutputDataTypeArg(&m_type).SetDefault("Float64");
302
303
0
    AddNodataArg(&m_nodata, true);
304
305
0
    AddValidationAction(
306
0
        [this]()
307
0
        {
308
0
            if (m_method.size() > 1 && m_method.size() != m_kernel.size())
309
0
            {
310
0
                ReportError(
311
0
                    CE_Failure, CPLE_AppDefined,
312
0
                    "The number of values for the 'method' argument should "
313
0
                    "be one or exactly the number of values of 'kernel'");
314
0
                return false;
315
0
            }
316
317
0
            if (m_size > 0)
318
0
            {
319
0
                for (const std::string &kernel : m_kernel)
320
0
                {
321
0
                    if (kernel == "gaussian")
322
0
                    {
323
0
                        if (m_size != 3 && m_size != 5)
324
0
                        {
325
0
                            ReportError(CE_Failure, CPLE_AppDefined,
326
0
                                        "Currently only size = 3 or 5 is "
327
0
                                        "supported for kernel '%s'",
328
0
                                        kernel.c_str());
329
0
                            return false;
330
0
                        }
331
0
                    }
332
0
                    else if (kernel == "unsharp-masking")
333
0
                    {
334
0
                        if (m_size != 5)
335
0
                        {
336
0
                            ReportError(CE_Failure, CPLE_AppDefined,
337
0
                                        "Currently only size = 5 is supported "
338
0
                                        "for kernel '%s'",
339
0
                                        kernel.c_str());
340
0
                            return false;
341
0
                        }
342
0
                    }
343
0
                    else if (kernel[0] == '[')
344
0
                    {
345
0
                        const CPLStringList aosValues(CSLTokenizeString2(
346
0
                            kernel.c_str(), "[] ,",
347
0
                            CSLT_STRIPLEADSPACES | CSLT_STRIPENDSPACES));
348
0
                        const double dfSize =
349
0
                            static_cast<double>(aosValues.size());
350
0
                        const int size =
351
0
                            static_cast<int>(std::floor(sqrt(dfSize) + 0.5));
352
0
                        if (m_size != size)
353
0
                        {
354
0
                            ReportError(CE_Failure, CPLE_AppDefined,
355
0
                                        "Value of 'size' argument (%d) "
356
0
                                        "inconsistent with the one deduced "
357
0
                                        "from the kernel matrix (%d)",
358
0
                                        m_size, size);
359
0
                            return false;
360
0
                        }
361
0
                    }
362
0
                    else if (m_size != 3 && kernel != "equal")
363
0
                    {
364
0
                        ReportError(CE_Failure, CPLE_AppDefined,
365
0
                                    "Currently only size = 3 is supported for "
366
0
                                    "kernel '%s'",
367
0
                                    kernel.c_str());
368
0
                        return false;
369
0
                    }
370
0
                }
371
0
            }
372
373
0
            return true;
374
0
        });
375
0
}
376
377
/************************************************************************/
378
/*                            GetKernelDef()                            */
379
/************************************************************************/
380
381
static KernelDef GetKernelDef(const std::string &name, bool normalizeCoefs,
382
                              double weightIfNotNormalized)
383
0
{
384
0
    auto it = oMapKernelNameToMatrix.find(name);
385
0
    CPLAssert(it != oMapKernelNameToMatrix.end());
386
0
    KernelDef def;
387
0
    def.size = it->second.first;
388
0
    int nSum = 0;
389
0
    for (const int nVal : it->second.second)
390
0
        nSum += nVal;
391
0
    const double dfWeight = normalizeCoefs
392
0
                                ? 1.0 / (static_cast<double>(nSum) +
393
0
                                         std::numeric_limits<double>::min())
394
0
                                : weightIfNotNormalized;
395
0
    for (const int nVal : it->second.second)
396
0
    {
397
0
        def.adfCoefficients.push_back(nVal * dfWeight);
398
0
    }
399
0
    return def;
400
0
}
401
402
/************************************************************************/
403
/*               GDALRasterNeighborsAlgorithm::RunStep()                */
404
/************************************************************************/
405
406
bool GDALRasterNeighborsAlgorithm::RunStep(GDALPipelineStepRunContext &)
407
0
{
408
0
    auto poSrcDS = m_inputDataset[0].GetDatasetRef();
409
0
    CPLAssert(!m_outputDataset.GetDatasetRef());
410
411
0
    CPLAssert(m_band <= poSrcDS->GetRasterCount());
412
413
0
    auto eType = GDALGetDataTypeByName(m_type.c_str());
414
0
    if (eType == GDT_Unknown)
415
0
    {
416
0
        eType = GDT_Float64;
417
0
    }
418
0
    std::vector<KernelDef> aKernelDefs(m_kernel.size());
419
0
    std::vector<bool> abNullCoefficientSum(m_kernel.size());
420
0
    for (size_t i = 0; i < m_kernel.size(); ++i)
421
0
    {
422
0
        const std::string &kernel = m_kernel[i];
423
0
        if (!kernel.empty() && kernel[0] == '[')
424
0
        {
425
0
            const CPLStringList aosValues(
426
0
                CSLTokenizeString2(kernel.c_str(), "[] ,",
427
0
                                   CSLT_STRIPLEADSPACES | CSLT_STRIPENDSPACES));
428
0
            const double dfSize = static_cast<double>(aosValues.size());
429
0
            KernelDef def;
430
0
            def.size = static_cast<int>(std::floor(sqrt(dfSize) + 0.5));
431
0
            double dfSum = 0;
432
0
            for (const char *pszC : cpl::Iterate(aosValues))
433
0
            {
434
0
                const double dfV = CPLAtof(pszC);
435
0
                dfSum += dfV;
436
                // Already validated to be numeric by the validation action
437
0
                def.adfCoefficients.push_back(dfV);
438
0
            }
439
0
            abNullCoefficientSum[i] = std::fabs(dfSum) < 1e-10;
440
0
            if (abNullCoefficientSum[i] && m_method.size() == m_kernel.size() &&
441
0
                m_method[i] == "mean")
442
0
            {
443
0
                ReportError(
444
0
                    CE_Failure, CPLE_AppDefined,
445
0
                    "Specifying method = 'mean' for a kernel whose sum of "
446
0
                    "coefficients is zero is not allowed. Use 'sum' instead");
447
0
                return false;
448
0
            }
449
0
            aKernelDefs[i] = std::move(def);
450
0
        }
451
0
    }
452
453
0
    if (m_method.empty())
454
0
    {
455
0
        for (size_t i = 0; i < m_kernel.size(); ++i)
456
0
        {
457
0
            const bool bIsZeroSumKernel =
458
0
                m_kernel[i] == "u" || m_kernel[i] == "v" ||
459
0
                m_kernel[i] == "edge1" || m_kernel[i] == "edge2" ||
460
0
                abNullCoefficientSum[i];
461
0
            m_method.push_back(bIsZeroSumKernel ? "sum" : "mean");
462
0
        }
463
0
    }
464
0
    else if (m_method.size() == 1)
465
0
    {
466
0
        const std::string lastValue(m_method.back());
467
0
        m_method.resize(m_kernel.size(), lastValue);
468
0
    }
469
470
0
    if (m_size == 0 && m_kernel[0][0] != '[')
471
0
        m_size = m_kernel[0] == "unsharp-masking" ? 5 : 3;
472
473
0
    for (size_t i = 0; i < m_kernel.size(); ++i)
474
0
    {
475
0
        const std::string &kernel = m_kernel[i];
476
0
        if (aKernelDefs[i].adfCoefficients.empty())
477
0
        {
478
0
            KernelDef def;
479
0
            if (kernel == "edge1" || kernel == "edge2" || kernel == "sharpen")
480
0
            {
481
0
                CPLAssert(m_size == 3);
482
0
                def = GetKernelDef(kernel, false, 1.0);
483
0
            }
484
0
            else if (kernel == "u" || kernel == "v")
485
0
            {
486
0
                CPLAssert(m_size == 3);
487
0
                def = GetKernelDef(kernel, false, 0.5);
488
0
            }
489
0
            else if (kernel == "equal")
490
0
            {
491
0
                def.size = m_size;
492
0
                const double dfWeight =
493
0
                    m_method[i] == "mean"
494
0
                        ? 1.0 / (static_cast<double>(m_size) * m_size +
495
0
                                 std::numeric_limits<double>::min())
496
0
                        : 1.0;
497
0
                def.adfCoefficients.resize(static_cast<size_t>(m_size) * m_size,
498
0
                                           dfWeight);
499
0
            }
500
0
            else if (kernel == "gaussian")
501
0
            {
502
0
                CPLAssert(m_size == 3 || m_size == 5);
503
0
                def = GetKernelDef(
504
0
                    m_size == 3 ? "gaussian-3x3" : "gaussian-5x5", true, 0.0);
505
0
            }
506
0
            else if (kernel == "unsharp-masking")
507
0
            {
508
0
                CPLAssert(m_size == 5);
509
0
                def = GetKernelDef("unsharp-masking-5x5", true, 0.0);
510
0
            }
511
0
            else
512
0
            {
513
0
                CPLAssert(false);
514
0
            }
515
0
            aKernelDefs[i] = std::move(def);
516
0
        }
517
0
    }
518
519
0
    auto vrt = GDALNeighborsCreateVRTDerived(poSrcDS, m_band, eType, m_nodata,
520
0
                                             m_method, aKernelDefs);
521
0
    const bool ret = vrt != nullptr;
522
0
    if (vrt)
523
0
    {
524
0
        m_outputDataset.Set(std::move(vrt));
525
0
    }
526
0
    return ret;
527
0
}
528
529
GDALRasterNeighborsAlgorithmStandalone::
530
0
    ~GDALRasterNeighborsAlgorithmStandalone() = default;
531
532
//! @endcond