Coverage Report

Created: 2026-10-28 07:09

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/gdal/apps/gdalalg_raster_as_features.cpp
Line
Count
Source
1
/******************************************************************************
2
*
3
 * Project:  GDAL
4
 * Purpose:  "as-features" step of "gdal pipeline"
5
 * Author:   Daniel Baston
6
 *
7
 ******************************************************************************
8
 * Copyright (c) 2025, ISciences, LLC
9
 *
10
 * SPDX-License-Identifier: MIT
11
 ****************************************************************************/
12
13
#include "gdalalg_raster_as_features.h"
14
#include "gdalalg_vector_pipeline.h"
15
16
#include "cpl_conv.h"
17
#include "gdal_priv.h"
18
#include "gdal_alg.h"
19
#include "ogrsf_frmts.h"
20
21
#include <cmath>
22
#include <limits>
23
#include <optional>
24
25
//! @cond Doxygen_Suppress
26
27
#ifndef _
28
0
#define _(x) (x)
29
#endif
30
31
GDALRasterAsFeaturesAlgorithm::GDALRasterAsFeaturesAlgorithm(
32
    bool standaloneStep)
33
0
    : GDALPipelineStepAlgorithm(
34
0
          NAME, DESCRIPTION, HELP_URL,
35
0
          ConstructorOptions()
36
0
              .SetStandaloneStep(standaloneStep)
37
0
              .SetAddUpsertArgument(false)
38
0
              .SetAddSkipErrorsArgument(false)
39
0
              .SetOutputFormatCreateCapability(GDAL_DCAP_CREATE))
40
0
{
41
0
    m_outputLayerName = "pixels";
42
43
0
    if (standaloneStep)
44
0
    {
45
0
        AddProgressArg(/* hidden = */ true);
46
0
        AddRasterInputArgs(false, false);
47
0
        AddVectorOutputArgs(false, false);
48
0
    }
49
0
    else
50
0
    {
51
0
        AddRasterHiddenInputDatasetArg();
52
0
        AddOutputLayerNameArg(/* hiddenForCLI = */ false,
53
0
                              /* shortNameOutputLayerAllowed = */ false);
54
0
    }
55
56
0
    AddBandArg(&m_bands);
57
0
    AddArg("geometry-type", 0, _("Geometry type"), &m_geomTypeName)
58
0
        .SetChoices("none", "point", "polygon")
59
0
        .SetDefault(m_geomTypeName);
60
0
    AddArg("skip-nodata", 0, _("Omit NoData pixels from the result"),
61
0
           &m_skipNoData);
62
0
    AddArg("include-xy", 0, _("Include fields for cell center coordinates"),
63
0
           &m_includeXY);
64
0
    AddArg("include-row-col", 0, _("Include columns for row and column"),
65
0
           &m_includeRowCol);
66
0
}
67
68
0
GDALRasterAsFeaturesAlgorithm::~GDALRasterAsFeaturesAlgorithm() = default;
69
70
GDALRasterAsFeaturesAlgorithmStandalone::
71
    ~GDALRasterAsFeaturesAlgorithmStandalone() = default;
72
73
namespace
74
{
75
struct RasterAsFeaturesOptions
76
{
77
    OGRwkbGeometryType geomType{wkbNone};
78
    bool includeXY{false};
79
    bool includeRowCol{false};
80
    bool skipNoData{false};
81
    std::vector<int> bands{};
82
    std::string outputLayerName{};
83
};
84
85
class GDALRasterAsFeaturesLayer final
86
    : public OGRLayer,
87
      public OGRGetNextFeatureThroughRaw<GDALRasterAsFeaturesLayer>
88
{
89
  public:
90
    static constexpr const char *ROW_FIELD = "ROW";
91
    static constexpr const char *COL_FIELD = "COL";
92
    static constexpr const char *X_FIELD = "CENTER_X";
93
    static constexpr const char *Y_FIELD = "CENTER_Y";
94
95
    DEFINE_GET_NEXT_FEATURE_THROUGH_RAW(GDALRasterAsFeaturesLayer)
96
97
    GDALRasterAsFeaturesLayer(GDALDataset &ds, RasterAsFeaturesOptions options)
98
0
        : m_ds(ds), m_it(GDALRasterBand::WindowIterator(
99
0
                        m_ds.GetRasterXSize(), m_ds.GetRasterYSize(),
100
0
                        m_ds.GetRasterXSize(), m_ds.GetRasterYSize(), 0, 0)),
101
0
          m_end(GDALRasterBand::WindowIterator(
102
0
              m_ds.GetRasterXSize(), m_ds.GetRasterYSize(),
103
0
              m_ds.GetRasterXSize(), m_ds.GetRasterYSize(), 1, 0)),
104
0
          m_defn(OGRFeatureDefnRefCountedPtr::makeInstance(
105
0
              options.outputLayerName.c_str())),
106
0
          m_includeXY(options.includeXY),
107
0
          m_includeRowCol(options.includeRowCol),
108
0
          m_excludeNoDataPixels(options.skipNoData)
109
0
    {
110
        // Features are read lazily from m_ds, so keep it alive as long as
111
        // this layer is.
112
0
        m_ds.Reference();
113
114
        // TODO: Handle Int64, UInt64
115
0
        m_ds.GetGeoTransform(m_gt);
116
117
0
        if (options.bands.empty())
118
0
        {
119
0
            const int nBands = m_ds.GetRasterCount();
120
0
            m_bands.resize(nBands);
121
0
            for (int i = 1; i <= nBands; i++)
122
0
            {
123
0
                m_bands[i - 1] = i;
124
0
            }
125
0
        }
126
0
        else
127
0
        {
128
0
            m_bands = std::move(options.bands);
129
0
        }
130
131
        // TODO: Handle per-band NoData values
132
0
        if (!m_bands.empty())
133
0
        {
134
0
            int hasNoData;
135
0
            double noData =
136
0
                m_ds.GetRasterBand(m_bands[0])->GetNoDataValue(&hasNoData);
137
0
            if (hasNoData)
138
0
            {
139
0
                m_noData = noData;
140
0
            }
141
0
        }
142
143
0
        SetDescription(options.outputLayerName.c_str());
144
0
        if (options.geomType == wkbNone)
145
0
        {
146
0
            m_defn->SetGeomType(wkbNone);
147
0
        }
148
0
        else
149
0
        {
150
0
            m_defn->GetGeomFieldDefn(0)->SetType(options.geomType);
151
0
            m_defn->GetGeomFieldDefn(0)->SetSpatialRef(ds.GetSpatialRef());
152
0
        }
153
154
0
        if (m_includeXY)
155
0
        {
156
0
            auto xField = std::make_unique<OGRFieldDefn>(X_FIELD, OFTReal);
157
0
            auto yField = std::make_unique<OGRFieldDefn>(Y_FIELD, OFTReal);
158
0
            m_defn->AddFieldDefn(std::move(xField));
159
0
            m_defn->AddFieldDefn(std::move(yField));
160
0
        }
161
0
        if (m_includeRowCol)
162
0
        {
163
0
            auto rowField =
164
0
                std::make_unique<OGRFieldDefn>(ROW_FIELD, OFTInteger);
165
0
            auto colField =
166
0
                std::make_unique<OGRFieldDefn>(COL_FIELD, OFTInteger);
167
0
            m_defn->AddFieldDefn(std::move(rowField));
168
0
            m_defn->AddFieldDefn(std::move(colField));
169
0
        }
170
0
        for (int band : m_bands)
171
0
        {
172
0
            CPLString fieldName = CPLSPrintf("BAND_%d", band);
173
0
            auto bandField =
174
0
                std::make_unique<OGRFieldDefn>(fieldName.c_str(), OFTReal);
175
0
            m_defn->AddFieldDefn(std::move(bandField));
176
0
            m_bandFields.push_back(m_defn->GetFieldIndex(fieldName));
177
0
        }
178
179
0
        GDALRasterAsFeaturesLayer::ResetReading();
180
0
    }
181
182
    ~GDALRasterAsFeaturesLayer() override
183
0
    {
184
0
        m_ds.ReleaseRef();
185
0
    }
186
187
    void ResetReading() override
188
0
    {
189
0
        if (m_ds.GetRasterCount() > 0)
190
0
        {
191
0
            GDALRasterBand *poFirstBand = m_ds.GetRasterBand(1);
192
0
            CPLAssert(poFirstBand);  // appease clang scan-build
193
0
            m_it = poFirstBand->IterateWindows().begin();
194
0
            m_end = poFirstBand->IterateWindows().end();
195
0
        }
196
0
    }
197
198
    bool TestCapability(const char *pszCap) const override
199
0
    {
200
0
        return EQUAL(pszCap, OLCFastFeatureCount) &&
201
0
               m_poFilterGeom == nullptr && m_poAttrQuery == nullptr &&
202
0
               !m_excludeNoDataPixels;
203
0
    }
204
205
    GIntBig GetFeatureCount(int bForce) override
206
0
    {
207
0
        if (m_poFilterGeom == nullptr && m_poAttrQuery == nullptr &&
208
0
            !m_excludeNoDataPixels)
209
0
        {
210
0
            return static_cast<GIntBig>(m_ds.GetRasterXSize()) *
211
0
                   m_ds.GetRasterYSize();
212
0
        }
213
0
        return OGRLayer::GetFeatureCount(bForce);
214
0
    }
215
216
    OGRFeatureDefn *GetLayerDefn() const override
217
0
    {
218
0
        return m_defn.get();
219
0
    }
220
221
    OGRFeature *GetNextRawFeature()
222
0
    {
223
0
        if (m_row >= m_window.nYSize && !NextWindow())
224
0
        {
225
0
            return nullptr;
226
0
        }
227
228
0
        std::unique_ptr<OGRFeature> feature;
229
230
0
        while (m_row < m_window.nYSize)
231
0
        {
232
0
            const double *pSrcVal = reinterpret_cast<double *>(m_buf.data()) +
233
0
                                    (m_bands.size() * m_row * m_window.nXSize +
234
0
                                     m_col * m_bands.size());
235
236
0
            const bool emitFeature =
237
0
                !m_excludeNoDataPixels || !IsNoData(*pSrcVal);
238
239
0
            if (emitFeature)
240
0
            {
241
0
                feature.reset(OGRFeature::CreateFeature(m_defn.get()));
242
243
0
                for (int fieldPos : m_bandFields)
244
0
                {
245
0
                    feature->SetField(fieldPos, *pSrcVal);
246
0
                    pSrcVal++;
247
0
                }
248
249
0
                const double line = m_window.nYOff + m_row;
250
0
                const double pixel = m_window.nXOff + m_col;
251
252
0
                if (m_includeRowCol)
253
0
                {
254
0
                    feature->SetField(ROW_FIELD, static_cast<GIntBig>(line));
255
0
                    feature->SetField(COL_FIELD, static_cast<GIntBig>(pixel));
256
0
                }
257
0
                if (m_includeXY)
258
0
                {
259
0
                    double x, y;
260
0
                    m_gt.Apply(pixel + 0.5, line + 0.5, &x, &y);
261
0
                    feature->SetField(X_FIELD, x);
262
0
                    feature->SetField(Y_FIELD, y);
263
0
                }
264
265
0
                std::unique_ptr<OGRGeometry> geom;
266
0
                const auto geomType = m_defn->GetGeomType();
267
0
                if (geomType == wkbPoint)
268
0
                {
269
0
                    double x, y;
270
0
                    m_gt.Apply(pixel + 0.5, line + 0.5, &x, &y);
271
272
0
                    geom = std::make_unique<OGRPoint>(x, y);
273
0
                    geom->assignSpatialReference(
274
0
                        m_defn->GetGeomFieldDefn(0)->GetSpatialRef());
275
0
                }
276
0
                else if (geomType == wkbPolygon)
277
0
                {
278
0
                    double x, y;
279
280
0
                    auto lr = std::make_unique<OGRLinearRing>();
281
282
0
                    m_gt.Apply(pixel, line, &x, &y);
283
0
                    lr->addPoint(x, y);
284
0
                    m_gt.Apply(pixel, line + 1, &x, &y);
285
0
                    lr->addPoint(x, y);
286
0
                    m_gt.Apply(pixel + 1, line + 1, &x, &y);
287
0
                    lr->addPoint(x, y);
288
0
                    m_gt.Apply(pixel + 1, line, &x, &y);
289
0
                    lr->addPoint(x, y);
290
0
                    m_gt.Apply(pixel, line, &x, &y);
291
0
                    lr->addPoint(x, y);
292
293
0
                    auto poly = std::make_unique<OGRPolygon>();
294
0
                    poly->addRing(std::move(lr));
295
0
                    geom = std::move(poly);
296
0
                    geom->assignSpatialReference(
297
0
                        m_defn->GetGeomFieldDefn(0)->GetSpatialRef());
298
0
                }
299
300
0
                feature->SetGeometry(std::move(geom));
301
0
            }
302
303
0
            m_col += 1;
304
0
            if (m_col >= m_window.nXSize)
305
0
            {
306
0
                m_col = 0;
307
0
                m_row++;
308
0
            }
309
310
0
            if (m_row >= m_window.nYSize)
311
0
            {
312
0
                NextWindow();
313
0
            }
314
315
0
            if (feature)
316
0
            {
317
0
                return feature.release();
318
0
            }
319
0
        }
320
321
0
        return nullptr;
322
0
    }
323
324
    CPL_DISALLOW_COPY_ASSIGN(GDALRasterAsFeaturesLayer)
325
326
  private:
327
    bool IsNoData(double x) const
328
0
    {
329
0
        if (!m_noData.has_value())
330
0
        {
331
0
            return false;
332
0
        }
333
334
0
        return m_noData.value() == x ||
335
0
               (std::isnan(m_noData.value()) && std::isnan(x));
336
0
    }
337
338
    bool NextWindow()
339
0
    {
340
0
        int nBandCount = static_cast<int>(m_bands.size());
341
342
0
        if (m_it == m_end)
343
0
        {
344
0
            return false;
345
0
        }
346
347
0
        if (m_ds.GetRasterXSize() == 0 || m_ds.GetRasterYSize() == 0)
348
0
        {
349
0
            return false;
350
0
        }
351
352
0
        m_window = *m_it;
353
0
        ++m_it;
354
355
0
        if (!m_bands.empty())
356
0
        {
357
0
            const auto nBufTypeSize = GDALGetDataTypeSizeBytes(m_bufType);
358
            if constexpr (sizeof(int) < sizeof(size_t))
359
0
            {
360
0
                if (m_window.nYSize > 0 &&
361
0
                    static_cast<size_t>(m_window.nXSize) >
362
0
                        std::numeric_limits<size_t>::max() / m_window.nYSize)
363
0
                {
364
0
                    CPLError(CE_Failure, CPLE_OutOfMemory,
365
0
                             "Failed to allocate buffer");
366
0
                    return false;
367
0
                }
368
0
            }
369
0
            const size_t nPixelCount =
370
0
                static_cast<size_t>(m_window.nXSize) * m_window.nYSize;
371
0
            if (static_cast<size_t>(nBandCount) * nBufTypeSize >
372
0
                std::numeric_limits<size_t>::max() / nPixelCount)
373
0
            {
374
0
                CPLError(CE_Failure, CPLE_OutOfMemory,
375
0
                         "Failed to allocate buffer");
376
0
                return false;
377
0
            }
378
0
            const size_t nBufSize = nPixelCount * nBandCount * nBufTypeSize;
379
0
            if (m_buf.size() < nBufSize)
380
0
            {
381
0
                try
382
0
                {
383
0
                    m_buf.resize(nBufSize);
384
0
                }
385
0
                catch (const std::exception &)
386
0
                {
387
0
                    CPLError(CE_Failure, CPLE_OutOfMemory,
388
0
                             "Failed to allocate buffer");
389
0
                    return false;
390
0
                }
391
0
            }
392
393
0
            const auto nPixelSpace =
394
0
                static_cast<GSpacing>(nBandCount) * nBufTypeSize;
395
0
            const auto eErr = m_ds.RasterIO(
396
0
                GF_Read, m_window.nXOff, m_window.nYOff, m_window.nXSize,
397
0
                m_window.nYSize, m_buf.data(), m_window.nXSize, m_window.nYSize,
398
0
                m_bufType, static_cast<int>(m_bands.size()), m_bands.data(),
399
0
                nPixelSpace, nPixelSpace * m_window.nXSize, nBufTypeSize,
400
0
                nullptr);
401
402
0
            if (eErr != CE_None)
403
0
            {
404
0
                CPLError(CE_Failure, CPLE_AppDefined,
405
0
                         "Failed to read raster data");
406
0
                return false;
407
0
            }
408
0
        }
409
410
0
        m_row = 0;
411
0
        m_col = 0;
412
413
0
        return true;
414
0
    }
415
416
    GDALDataset &m_ds;
417
    std::vector<GByte> m_buf{};
418
    const GDALDataType m_bufType = GDT_Float64;
419
    GDALGeoTransform m_gt{};
420
    std::optional<double> m_noData{std::nullopt};
421
422
    std::vector<int> m_bands{};
423
    std::vector<int> m_bandFields{};
424
425
    GDALRasterBand::WindowIterator m_it;
426
    GDALRasterBand::WindowIterator m_end;
427
    GDALRasterWindow m_window{};
428
429
    int m_row{0};
430
    int m_col{0};
431
432
    const OGRFeatureDefnRefCountedPtr m_defn;
433
    bool m_includeXY;
434
    bool m_includeRowCol;
435
    bool m_excludeNoDataPixels;
436
};
437
438
}  // namespace
439
440
bool GDALRasterAsFeaturesAlgorithm::RunStep(GDALPipelineStepRunContext &)
441
0
{
442
0
    auto poSrcDS = m_inputDataset[0].GetDatasetRef();
443
444
0
    RasterAsFeaturesOptions options;
445
0
    options.geomType = m_geomTypeName == "point"     ? wkbPoint
446
0
                       : m_geomTypeName == "polygon" ? wkbPolygon
447
0
                                                     : wkbNone;
448
0
    options.includeRowCol = m_includeRowCol;
449
0
    options.includeXY = m_includeXY;
450
0
    options.skipNoData = m_skipNoData;
451
0
    options.outputLayerName = m_outputLayerName;
452
453
0
    if (!m_bands.empty())
454
0
    {
455
0
        options.bands = std::move(m_bands);
456
0
    }
457
458
0
    auto poLayer =
459
0
        std::make_unique<GDALRasterAsFeaturesLayer>(*poSrcDS, options);
460
0
    auto poRetDS = std::make_unique<GDALVectorOutputDataset>(nullptr);
461
0
    poRetDS->AddLayer(std::move(poLayer));
462
463
0
    m_outputDataset.Set(std::move(poRetDS));
464
465
0
    return true;
466
0
}
467
468
//! @endcond