gdal icon indicating copy to clipboard operation
gdal copied to clipboard

gdalwarp VRT NoData issue

Open Novskilol opened this issue 5 months ago • 5 comments

What is the bug?

Gdalwarp in c++ when using VRT seems to ignore nans despite telling gdal explicitly

Steps to reproduce the issue

I have a simple 61x61 dataset generated randomly :

Image

NoData value is nan, when I project it using this command line :

gdalwarp -s_srs "+proj=utm +zone=15 +north +datum=WGS84 +units=m +no_defs" -t_srs "+proj=utm +zone=15 +north +datum=WGS84 +units=m +no_defs" -tr 60 60 -r bilinear -srcnodata nan -ot Float64 -overwrite -of GTiff input_generated.tif output_gdal.tif

everything works fine as you can see :

Image

bilinear interpolation didn't take into account nan values due to "-srcnodata nan"

now when I try to do it programmaticaly with a VRT :

  1. opening the dataset
// Create the in-memory dataset
  GDALDriver* memDriver = GetGDALDriverManager()->GetDriverByName("MEM");

  dataset.reset(memDriver->Create("", nCols, nRows, 1, GDT_Float64, NULL));

  // Get the raster band
  GDALRasterBand* band = dataset->GetRasterBand(1);

  // Set NoData value for NaN handling
  band->SetNoDataValue(std::numeric_limits<double>::quiet_NaN());
  // Copy data from xarray to GDAL dataset
  // Note: GDAL expects data in row-major order, same as xtensor default
  CPLErr result = band->RasterIO(GF_Write, 0, 0, nCols, nRows, const_cast<double*>(data.data()),
                                 nCols, nRows, GDT_Float64, 0, 0);

  return dataset;

Then projection using the required NoData options :


projectedDataset.reset(
        memDriver->Create("", nCols, nRows, inputDataset->GetRasterCount(), GDT_Float64, NULL));
    if (projectedDataset == NULL) {
        LogHandler::handleLog(
            "I-008", vector<string>{"Failed to open projected dataset for " + datasetType, "open"});
        return;
    }

    // Set the projection to UTM
    OGRSpatialReference projectedSRS;
    projectedSRS.SetWellKnownGeogCS("WGS84");
    projectedSRS.SetUTM(ProjectionInfo::get().getUtmZone(), ProjectionInfo::get().getNorthing());
    projectedDataset->SetSpatialRef(&projectedSRS);

    auto geoTransform = ProjectionInfo::get().getGeoTransform();

    // Set the geo transform
    projectedDataset->SetGeoTransform(geoTransform.data());

    std::unique_ptr<GDALWarpOptions, decltype(&GDALDestroyWarpOptions)> warpOptions{
        nullptr, GDALDestroyWarpOptions};

    // get Nodata value
    GDALRasterBand *srcBand = inputDataset->GetRasterBand(1);
    int hasNoData;
    double noDataValue = srcBand->GetNoDataValue(&hasNoData);
    cout << "NoData value: " << noDataValue << endl;
    cout << "hasNoData prep value: " << hasNoData << endl;
    int nBands = inputDataset->GetRasterCount();

    cout << "nBands: " << nBands << endl;

    // Setup warp options
    warpOptions.reset(GDALCreateWarpOptions());
    warpOptions->hSrcDS = inputDataset;
    warpOptions->hDstDS = projectedDataset.get();

    // CRITICAL: Set these options to handle nodata properly during bilinear interpolation
    warpOptions->papszWarpOptions =
        CSLSetNameValue(warpOptions->papszWarpOptions, "INIT_DEST", "NO_DATA");
    warpOptions->papszWarpOptions =
        CSLSetNameValue(warpOptions->papszWarpOptions, "UNIFIED_SRC_NODATA", "YES");
    warpOptions->papszWarpOptions =
        CSLSetNameValue(warpOptions->papszWarpOptions, "SAMPLE_GRID", "YES");

    // Only set nodata if the source actually has nodata defined
    if (hasNoData == 1) {
        // Allocate arrays for source and destination nodata values
        warpOptions->padfSrcNoDataReal = (double *)CPLMalloc(sizeof(double) * nBands);
        warpOptions->padfDstNoDataReal = (double *)CPLMalloc(sizeof(double) * nBands);
        cout << "warp options allocated" << endl;

        // Set nodata values for each band
        for (int i = 0; i < nBands; i++) {
            // Also set nodata on the destination bands
            GDALRasterBand *dstBand = projectedDataset->GetRasterBand(i + 1);
            dstBand->SetNoDataValue(noDataValue);
            cout << "setting nodata value for output band " << i << " to " << noDataValue << endl;

            cout << "setting nodata value for band " << i << endl;
            warpOptions->padfSrcNoDataReal[i] = noDataValue;
            warpOptions->padfDstNoDataReal[i] = noDataValue;
        }
    } else {
        // If no nodata is defined, you might want to set a specific value
        // or leave these as NULL to avoid nodata handling issues
        cout << "No nodata value defined in source dataset" << endl;
    }

    warpOptions->pfnProgress = GDALTermProgress;

    // Establish reprojection transformer
    warpOptions->pTransformerArg = GDALCreateGenImgProjTransformer(
        inputDataset, GDALGetProjectionRef(inputDataset), projectedDataset.get(),
        GDALGetProjectionRef(projectedDataset.get()), FALSE, 0.0, 1);
    warpOptions->pfnTransformer = GDALGenImgProjTransform;
    warpOptions->eResampleAlg = resampleAlg;
    warpOptions->pfnProgress = GDALDummyProgress;  // Add this to suppress progress messages
    warpOptions->pProgressArg = nullptr;

    // Initialize and execute the warp operation
    try {
        LogCapture capture("GDAL");
        capture.start();

        GDALWarpOperation operation;
        operation.Initialize(warpOptions.get());
        operation.ChunkAndWarpImage(0, 0, GDALGetRasterXSize(projectedDataset.get()),
                                    GDALGetRasterYSize(projectedDataset.get()));

        capture.stop();
    } catch (const std::exception &e) {
        throw GenericException("I-016", {"GDAL ", string(e.what())});
    }

The prints inform me that noData is indeed nan and that it was correctly set inside input and output dataset, now the output :

Image

we can see that nan values propagated, so they were not correctly interpreted as nans, despite NoData being shown as nan when opening them inside qgis.

What am I doing wrong? Or is it a bug? ( Gdal downloaded from conan center, version 3.8.3 )

Versions and provenance

3.8.3

Additional context

No response

Novskilol avatar Jul 23 '25 12:07 Novskilol

it's not actually a problem of "MEM" driver, I have the same issue when opening the dataset from a .tif file and writing to a .tif dataset.

Another interesting information : if I switch nan values to 9999 and set NoData value as 9999 : gdalwarp tool still works, but c++ warp interprets 9999 as values and doesn't work.

Novskilol avatar Jul 23 '25 13:07 Novskilol

Apart from warpOptions->nBandCount = 1 which is missing, nothing obvious strikes me as wrong. I'd suggest you use the GDALWarp() higher level API. Otherwise please include a fully self-contained C/C++ reproducer (ie something that just needs to be compiled)

rouault avatar Jul 23 '25 16:07 rouault

Thanks for your answer, somehow warpOptions->nBandCount = 1 made the program idle forever...

I asked LLMS for a simpler version using GDALReprojectImage :

void GenerateUtils::testProjectionWithGDALWarp() {
    int nRows = 61, nCols = 61;
    double noData = 9999.0;

    xt::xarray<double> data = xt::random::randn<double>({nRows, nCols}) * 10.0 + 283.0;
    data = xt::where(xt::random::rand<double>({nRows, nCols}) > 0.5, data, noData);

    string inputPath = "test_input.tif";
    RasterUtils::writeSingleBandXarrayToGeoTiff<double>(data, inputPath, true, noData);

    GDALDataset* srcDS = (GDALDataset*)GDALOpen(inputPath.c_str(), GA_ReadOnly);

    OGRSpatialReference srcSRS, dstSRS;
    srcSRS.SetWellKnownGeogCS("WGS84");
    srcSRS.SetUTM(15, 1);
    srcDS->SetSpatialRef(&srcSRS);

    double srcGeoTransform[6] = {699960, 1800.0, 0.0, 5700000, 0.0, -1800.0};
    srcDS->SetGeoTransform(srcGeoTransform);

    dstSRS.SetWellKnownGeogCS("WGS84");
    dstSRS.SetUTM(15, 1);

    auto geoTransform = ProjectionInfo::get().getGeoTransform();

    // Define output dimensions BEFORE creating VRT
    int outRows = 1830;
    int outCols = 1830;

    char* dstWKT = nullptr;
    dstSRS.exportToWkt(&dstWKT);
    GDALDriver* driver = GetGDALDriverManager()->GetDriverByName("GTiff");
    GDALDataset* finalDS =
        driver->Create("test_output.tif", outCols, outRows, 1, GDT_Float64, nullptr);

    finalDS->SetSpatialRef(&dstSRS);
    finalDS->SetGeoTransform(geoTransform.data());
    finalDS->GetRasterBand(1)->SetNoDataValue(noData);

    GDALReprojectImage(srcDS, nullptr, finalDS, dstWKT, GRA_Bilinear, 0.0, 0.0, nullptr, nullptr,
                       nullptr);

    xt::xarray<double> result = xt::zeros<double>({outRows, outCols});
    finalDS->GetRasterBand(1)->RasterIO(GF_Read, 0, 0, outCols, outRows, result.data(), outCols,
                                        outRows, GDT_Float64, 0, 0);

    cout << "Result shape: " << xt::adapt(result.shape()) << endl;

    GDALClose(srcDS);
    GDALClose(finalDS);
    CPLFree(dstWKT);
    exit(0);
}

and it worked, posting here so other people can see the solution.

Novskilol avatar Jul 24 '25 09:07 Novskilol

I thought it was OK, but the issue came from somewhere else, it came from the fact that the input raster had multiple bands :

I made this reproductible small code for testing


#include "gdal.h"
#include "gdal_priv.h"
#include "gdalwarper.h"
#include "xtensor/xadapt.hpp"
#include "xtensor/xarray.hpp"
#include "xtensor/xio.hpp"
#include "xtensor/xmanipulation.hpp"
#include "xtensor/xmath.hpp"
#include "xtensor/xrandom.hpp"
#include "xtensor/xview.hpp"


void GenerateUtils::testProjectionWithGDALWarpGeogToUtm(int nBands) {
    int nRows = 61, nCols = 61;
    double noData = std::numeric_limits<double>::quiet_NaN();

    xt::xarray<double> data[nBands];
    // Create test data and double it
    for (int i = 0; i < nBands; i++) {
        data[i] = xt::random::randn<double>({nRows, nCols}) * 10.0 + 283.0;
        data[i] = xt::where(xt::random::rand<double>({nRows, nCols}) > 0.5, data[i], noData);
    }

    // Basic geographic projection setup
    const char* srcWKT =
        "GEOGCS[\"WGS84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS "
        "84\",6378137,298.257223563]],PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433]]";
    double srcGeoTransform[6] = {-91.0, 0.05, 0.0, 51.5, 0.0, -0.025};

    // Create input dataset
    GDALDriver* driver = GetGDALDriverManager()->GetDriverByName("GTiff");
    GDALDataset* srcDS = driver->Create(("test_input_" + to_string(nBands) + "_bands.tif").c_str(),
                                        nCols, nRows, nBands, GDT_Float64, nullptr);
    OGRSpatialReference srcSRS;
    char* pSrcWKT = CPLStrdup(srcWKT);
    srcSRS.importFromWkt(&pSrcWKT);
    srcDS->SetSpatialRef(&srcSRS);
    srcDS->SetGeoTransform(srcGeoTransform);

    for (int i = 1; i <= srcDS->GetRasterCount(); i++) {
        srcDS->GetRasterBand(i)->SetNoDataValue(noData);
        srcDS->GetRasterBand(i)->RasterIO(GF_Write, 0, 0, nCols, nRows, data[i - 1].data(), nCols,
                                          nRows, GDT_Float64, 0, 0);
    }

    // Setup UTM destination
    OGRSpatialReference dstSRS;
    dstSRS.SetWellKnownGeogCS("WGS84");
    dstSRS.SetUTM(15, 1);

    // Create output dataset
    int outRows = 915, outCols = 915;
    auto geoTransform = ProjectionInfo::get().getGeoTransform();
    GDALDataset* dstDS = driver->Create(("test_output_" + to_string(nBands) + "_bands.tif").c_str(),
                                        outCols, outRows, nBands, GDT_Float64, nullptr);
    dstDS->SetSpatialRef(&dstSRS);
    dstDS->SetGeoTransform(geoTransform.data());

    // Initialize destination bands with NoData
    for (int i = 1; i <= dstDS->GetRasterCount(); i++) {
        dstDS->GetRasterBand(i)->SetNoDataValue(noData);
        // Initialize the entire band with NoData values
        double* buffer = new double[outCols * outRows];
        std::fill_n(buffer, outCols * outRows, noData);
        dstDS->GetRasterBand(i)->RasterIO(GF_Write, 0, 0, outCols, outRows, buffer, outCols,
                                          outRows, GDT_Float64, 0, 0);
        delete[] buffer;
    }

    // Setup warp options
    char* dstWKT = nullptr;
    dstSRS.exportToWkt(&dstWKT);

    char** papszWarpOptions = NULL;
    papszWarpOptions = CSLSetNameValue(papszWarpOptions, "INIT_DEST", "NO_DATA");
    papszWarpOptions = CSLSetNameValue(papszWarpOptions, "WRITE_FLUSH", "YES");
    papszWarpOptions = CSLSetNameValue(papszWarpOptions, "SKIP_NOSOURCE", "YES");
    papszWarpOptions = CSLSetNameValue(papszWarpOptions, "UNIFIED_SRC_NODATA", "NO");

    // Create and configure GDALWarpOptions
    GDALWarpOptions* warpOptions = GDALCreateWarpOptions();
    warpOptions->papszWarpOptions = papszWarpOptions;
    warpOptions->hSrcDS = srcDS;
    warpOptions->hDstDS = dstDS;
    warpOptions->eResampleAlg = GRA_Bilinear;

    // Set band count and allocate band arrays
    int bandCount = srcDS->GetRasterCount();
    warpOptions->nBandCount = bandCount;

    // Allocate band mapping arrays
    warpOptions->panSrcBands = (int*)CPLMalloc(sizeof(int) * bandCount);
    warpOptions->panDstBands = (int*)CPLMalloc(sizeof(int) * bandCount);

    // Set band mapping (1-based indexing for GDAL)
    for (int i = 0; i < bandCount; i++) {
        warpOptions->panSrcBands[i] = i + 1;
        warpOptions->panDstBands[i] = i + 1;
    }

    // Allocate and set NoData values for each band
    warpOptions->padfSrcNoDataReal = (double*)CPLMalloc(sizeof(double) * bandCount);
    warpOptions->padfDstNoDataReal = (double*)CPLMalloc(sizeof(double) * bandCount);
    warpOptions->padfSrcNoDataImag = nullptr;
    warpOptions->padfDstNoDataImag = nullptr;

    // Set NoData values for each band
    for (int i = 0; i < bandCount; i++) {
        warpOptions->padfSrcNoDataReal[i] = noData;
        warpOptions->padfDstNoDataReal[i] = noData;
    }

    // Perform reprojection
    CPLErr err = GDALReprojectImage(srcDS, nullptr, dstDS, dstWKT, GRA_Bilinear, 0.0, 0.0, nullptr,
                                    nullptr, warpOptions);

    // Force write to disk
    GDALFlushCache(dstDS);

    // Cleanup
    GDALDestroyWarpOptions(warpOptions);

    GDALClose(srcDS);
    GDALClose(dstDS);
}

this function generate a random array of values ( nRows * nCols), for each band if you call it with 1 band it will work perfectly :

Image

but as soon as you have 2 or more bands :

Image

it seems like papszWarpOptions = CSLSetNameValue(papszWarpOptions, "UNIFIED_SRC_NODATA", "NO"); is completely ignored

Novskilol avatar Aug 05 '25 14:08 Novskilol

it seems like papszWarpOptions = CSLSetNameValue(papszWarpOptions, "UNIFIED_SRC_NODATA", "NO"); is completely ignored

From the image, it isn't obvious at all to me what the bug is. That may be an issue with your viewer or how you set it up ? If you want to make a reproducer to analyze, you should create very small rasters, like input_band1 = [1, 2, 1, 3] input_band2 = [1, 1, 2, 4] and nodata_band1 = nodata_band2 = 1

rouault avatar Aug 19 '25 11:08 rouault