Repository: OSGeo/gdal
Pull request #12087: gdallocationinfo: Allow querying raster corner points
Author: login: dbaston
Created: 2025-04-05T18:04:09Z | Merged: 2025-04-08T18:13:59Z

Applied on top of https://github.com/OSGeo/gdal/pull/12080

As discussed in https://github.com/OSGeo/gdal/pull/12080#discussion_r2028790673 


Diff:
diff --git a/alg/gdal_interpolateatpoint.cpp b/alg/gdal_interpolateatpoint.cpp
index 171106f3fb03..36b623b89825 100644
--- a/alg/gdal_interpolateatpoint.cpp
+++ b/alg/gdal_interpolateatpoint.cpp
@@ -124,13 +124,20 @@ bool GDALInterpExtractValuesWindow(GDALRasterBand *pBand,
             // Compose the cached block to the final buffer
             for (int j = 0; j < nLinesToCopy; j++)
             {
-                memcpy(padfAsDouble + ((nFirstLineInOutput + j) * nWidth +
-                                       nFirstColInOutput) *
-                                          nTypeFactor,
-                       poValue->data() +
-                           ((nFirstLineInCachedBlock + j) * nReqXSize +
-                            nFirstColInCachedBlock) *
-                               nTypeFactor,
+                const size_t dstOffset =
+                    (static_cast<size_t>(nFirstLineInOutput + j) * nWidth +
+                     nFirstColInOutput) *
+                    nTypeFactor;
+                const size_t srcOffset =
+                    (static_cast<size_t>(nFirstLineInCachedBlock + j) *
+                         nReqXSize +
+                     nFirstColInCachedBlock) *
+                    nTypeFactor;
+                if (srcOffset + nColsToCopy * nTypeFactor > poValue->size())
+                {
+                    return false;
+                }
+                memcpy(padfAsDouble + dstOffset, poValue->data() + srcOffset,
                        nColsToCopy * sizeof(T));
             }
         }
@@ -158,9 +165,20 @@ template <typename T>
 bool GDALInterpolateAtPointImpl(GDALRasterBand *pBand,
                                 GDALRIOResampleAlg eResampleAlg,
                                 std::unique_ptr<DoublePointsCache> &cache,
-                                const double dfXIn, const double dfYIn, T &out)
+                                double dfXIn, double dfYIn, T &out)
 {
     const gdal::Vector2i rasterSize{pBand->GetXSize(), pBand->GetYSize()};
+
+    if (eResampleAlg == GRIORA_NearestNeighbour)
+    {
+        // Allow input coordinates right at the bottom or right edge
+        // with GRIORA_NearestNeighbour.
+        // "introduce" them in the pixel of the image.
+        if (dfXIn >= rasterSize.x() && dfXIn <= rasterSize.x() + 1e-5)
+            dfXIn -= 0.25;
+        if (dfYIn >= rasterSize.y() && dfYIn <= rasterSize.y() + 1e-5)
+            dfYIn -= 0.25;
+    }
     const gdal::Vector2d inLoc{dfXIn, dfYIn};
 
     int bGotNoDataValue = FALSE;
diff --git a/apps/gdallocationinfo.cpp b/apps/gdallocationinfo.cpp
index bcd6e5225141..47bf38b1391d 100644
--- a/apps/gdallocationinfo.cpp
+++ b/apps/gdallocationinfo.cpp
@@ -439,8 +439,9 @@ MAIN_START(argc, argv)
 
         bool bPixelReport = true;
 
-        if (iPixel < 0 || iLine < 0 || iPixel >= GDALGetRasterXSize(hSrcDS) ||
-            iLine >= GDALGetRasterYSize(hSrcDS))
+        if (iPixel < 0 || iLine < 0 ||
+            dfPixel > static_cast<double>(GDALGetRasterXSize(hSrcDS) + 1e-5) ||
+            dfLine > static_cast<double>(GDALGetRasterYSize(hSrcDS) + 1e-5))
         {
             if (bAsXML)
                 osXML += "<Alert>Location is off this file! No further details "
diff --git a/autotest/gcore/interpolateatpoint.py b/autotest/gcore/interpolateatpoint.py
index e89c64c1041f..7df424baa5ee 100755
--- a/autotest/gcore/interpolateatpoint.py
+++ b/autotest/gcore/interpolateatpoint.py
@@ -44,6 +44,25 @@ def test_interpolateatpoint_outofrange():
     assert res is None
     res = ds.GetRasterBand(1).InterpolateAtPoint(10, 1200, gdal.GRIORA_Bilinear)
     assert res is None
+    res = ds.GetRasterBand(1).InterpolateAtPoint(-1, 0, gdal.GRIORA_NearestNeighbour)
+    assert res is None
+    res = ds.GetRasterBand(1).InterpolateAtPoint(0, -0.5, gdal.GRIORA_NearestNeighbour)
+    assert res is None
+
+
+def test_interpolateatpoint_right_at_border():
+
+    ds = gdal.Open("data/byte.tif")
+    res = ds.GetRasterBand(1).InterpolateAtPoint(20, 20, gdal.GRIORA_NearestNeighbour)
+    assert res == pytest.approx(107.0, 1e-5)
+    res = ds.GetRasterBand(1).InterpolateAtPoint(18, 20, gdal.GRIORA_NearestNeighbour)
+    assert res == pytest.approx(99.0, 1e-5)
+    res = ds.GetRasterBand(1).InterpolateAtPoint(20, 18, gdal.GRIORA_NearestNeighbour)
+    assert res == pytest.approx(123.0, 1e-5)
+    res = ds.GetRasterBand(1).InterpolateAtPoint(20, 20, gdal.GRIORA_Bilinear)
+    assert res == pytest.approx(107.0, 1e-5)
+    res = ds.GetRasterBand(1).InterpolateAtPoint(20.1, 20.1, gdal.GRIORA_Bilinear)
+    assert res is None
 
 
 @gdaltest.disable_exceptions()
@@ -444,3 +463,23 @@ def test_interpolateatgeolocation_1():
         assert ungeoreferenced_ds.GetRasterBand(1).InterpolateAtGeolocation(
             0, 0, None, gdal.GRIORA_NearestNeighbour
         )
+
+
+def test_interpolateatgeolocation_corners():
+
+    gdaltest.importorskip_gdal_array()
+
+    ds = gdal.Open("data/byte.tif")
+    band = ds.GetRasterBand(1)
+    data = band.ReadAsArray()
+
+    values = {}
+    for loc, coord in gdal.Info(ds, format="json")["cornerCoordinates"].items():
+        values[loc] = band.InterpolateAtGeolocation(
+            coord[0], coord[1], None, gdal.GRIORA_NearestNeighbour
+        )
+
+    assert values["upperLeft"] == data[0, 0]
+    assert values["upperRight"] == data[0, -1]
+    assert values["lowerLeft"] == data[-1, 0]
+    assert values["lowerRight"] == data[-1, -1]
diff --git a/autotest/utilities/test_gdallocationinfo.py b/autotest/utilities/test_gdallocationinfo.py
index d9934ff0549c..9141dc6e4aec 100755
--- a/autotest/utilities/test_gdallocationinfo.py
+++ b/autotest/utilities/test_gdallocationinfo.py
@@ -101,6 +101,20 @@ def test_gdallocationinfo_4(gdallocationinfo_path):
     assert ret.startswith(expected_ret)
 
 
+# Test -geoloc at lower right corner
+def test_gdallocationinfo_lr(gdallocationinfo_path):
+
+    ret = gdaltest.runexternal(
+        gdallocationinfo_path + " -geoloc ../gcore/data/byte.tif 441920.000 3750120.000"
+    )
+    ret = ret.replace("\r\n", "\n")
+    expected_ret = """Report:
+  Location: (20P,20L)
+  Band 1:
+    Value: 107"""
+    assert ret.startswith(expected_ret)
+
+
 ###############################################################################
 # Test -lifonly
 


Reviews:
user: login: rouault
body: some suggestions to likely fix the test failure on Mac ARM64 where there must be some numeric precision issue
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-05T19:26:45Z
user: login: jjimenezshaw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-05T21:33:17Z
user: login: rouault
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-05T21:35:26Z
user: login: jjimenezshaw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-05T21:41:47Z

Review comments:
diff_hunk: @@ -158,9 +165,20 @@ template <typename T>
 bool GDALInterpolateAtPointImpl(GDALRasterBand *pBand,
                                 GDALRIOResampleAlg eResampleAlg,
                                 std::unique_ptr<DoublePointsCache> &cache,
-                                const double dfXIn, const double dfYIn, T &out)
+                                double dfXIn, double dfYIn, T &out)
 {
     const gdal::Vector2i rasterSize{pBand->GetXSize(), pBand->GetYSize()};
+
+    if (eResampleAlg == GRIORA_NearestNeighbour)
+    {
+        // Allow input coordinates right at the bottom or right edge
+        // with GRIORA_NearestNeighbour.
+        // "introduce" them in the pixel of the image.
+        if (dfXIn == rasterSize.x())
path: alg/gdal_interpolateatpoint.cpp
user: login: rouault
body: ```suggestion
        if (dfXIn >= rasterSize.x() && dfXIn <= rasterSize.x()  + 1e-5)
```
created_at: 2025-04-05T19:25:44Z
author_association: MEMBER
diff_hunk: @@ -158,9 +165,20 @@ template <typename T>
 bool GDALInterpolateAtPointImpl(GDALRasterBand *pBand,
                                 GDALRIOResampleAlg eResampleAlg,
                                 std::unique_ptr<DoublePointsCache> &cache,
-                                const double dfXIn, const double dfYIn, T &out)
+                                double dfXIn, double dfYIn, T &out)
 {
     const gdal::Vector2i rasterSize{pBand->GetXSize(), pBand->GetYSize()};
+
+    if (eResampleAlg == GRIORA_NearestNeighbour)
+    {
+        // Allow input coordinates right at the bottom or right edge
+        // with GRIORA_NearestNeighbour.
+        // "introduce" them in the pixel of the image.
+        if (dfXIn == rasterSize.x())
+            dfXIn -= 0.25;
+        if (dfYIn == rasterSize.y())
path: alg/gdal_interpolateatpoint.cpp
user: login: rouault
body: ```suggestion
        if (dfYIn >= rasterSize.y() && dfYIn <= rasterSize.y()  + 1e-5)
```
created_at: 2025-04-05T19:26:01Z
author_association: MEMBER
diff_hunk: @@ -439,8 +439,9 @@ MAIN_START(argc, argv)
 
         bool bPixelReport = true;
 
-        if (iPixel < 0 || iLine < 0 || iPixel >= GDALGetRasterXSize(hSrcDS) ||
-            iLine >= GDALGetRasterYSize(hSrcDS))
+        if (iPixel < 0 || iLine < 0 ||
+            dfPixel > static_cast<double>(GDALGetRasterXSize(hSrcDS)) ||
+            dfLine > static_cast<double>(GDALGetRasterYSize(hSrcDS)))
path: apps/gdallocationinfo.cpp
user: login: rouault
body: ```suggestion
            dfPixel > static_cast<double>(GDALGetRasterXSize(hSrcDS) + 1e-5) ||
            dfLine > static_cast<double>(GDALGetRasterYSize(hSrcDS) + 1e-5))
```
created_at: 2025-04-05T19:26:23Z
author_association: MEMBER
diff_hunk: @@ -158,9 +165,20 @@ template <typename T>
 bool GDALInterpolateAtPointImpl(GDALRasterBand *pBand,
                                 GDALRIOResampleAlg eResampleAlg,
                                 std::unique_ptr<DoublePointsCache> &cache,
-                                const double dfXIn, const double dfYIn, T &out)
+                                double dfXIn, double dfYIn, T &out)
 {
     const gdal::Vector2i rasterSize{pBand->GetXSize(), pBand->GetYSize()};
+
+    if (eResampleAlg == GRIORA_NearestNeighbour)
+    {
+        // Allow input coordinates right at the bottom or right edge
+        // with GRIORA_NearestNeighbour.
+        // "introduce" them in the pixel of the image.
+        if (dfXIn == rasterSize.x())
path: alg/gdal_interpolateatpoint.cpp
user: login: jjimenezshaw
body: Should I do this in #12080 ?
created_at: 2025-04-05T21:33:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -158,9 +165,20 @@ template <typename T>
 bool GDALInterpolateAtPointImpl(GDALRasterBand *pBand,
                                 GDALRIOResampleAlg eResampleAlg,
                                 std::unique_ptr<DoublePointsCache> &cache,
-                                const double dfXIn, const double dfYIn, T &out)
+                                double dfXIn, double dfYIn, T &out)
 {
     const gdal::Vector2i rasterSize{pBand->GetXSize(), pBand->GetYSize()};
+
+    if (eResampleAlg == GRIORA_NearestNeighbour)
+    {
+        // Allow input coordinates right at the bottom or right edge
+        // with GRIORA_NearestNeighbour.
+        // "introduce" them in the pixel of the image.
+        if (dfXIn == rasterSize.x())
path: alg/gdal_interpolateatpoint.cpp
user: login: rouault
body: > Should I do this in #12080 ?

maybe here, if you can push to Dan's branch
created_at: 2025-04-05T21:35:26Z
author_association: MEMBER
diff_hunk: @@ -158,9 +165,20 @@ template <typename T>
 bool GDALInterpolateAtPointImpl(GDALRasterBand *pBand,
                                 GDALRIOResampleAlg eResampleAlg,
                                 std::unique_ptr<DoublePointsCache> &cache,
-                                const double dfXIn, const double dfYIn, T &out)
+                                double dfXIn, double dfYIn, T &out)
 {
     const gdal::Vector2i rasterSize{pBand->GetXSize(), pBand->GetYSize()};
+
+    if (eResampleAlg == GRIORA_NearestNeighbour)
+    {
+        // Allow input coordinates right at the bottom or right edge
+        // with GRIORA_NearestNeighbour.
+        // "introduce" them in the pixel of the image.
+        if (dfXIn == rasterSize.x())
path: alg/gdal_interpolateatpoint.cpp
user: login: jjimenezshaw
body: > > Should I do this in #12080 ?
> 
> maybe here, if you can push to Dan's branch

Ups, I did in https://github.com/OSGeo/gdal/pull/12080 before reading your message.
created_at: 2025-04-05T21:41:47Z
author_association: CONTRIBUTOR

PR comments:
user: login: coveralls
created_at: 2025-04-05T19:43:21Z
body: 
[![Coverage Status](https://coveralls.io/builds/73107136/badge)](https://coveralls.io/builds/73107136)

coverage: 70.589% (-0.001%) from 70.59%
when pulling **9d9f08e7784d43f3a2bf3f87349da0c6ce628b8b on dbaston:locinfo-corners**
into **c7cce2313817aa3e1467ac93288a6fb30e151326 on OSGeo:master**.

author_association: COLLABORATOR
user: login: ctoney
created_at: 2025-04-05T20:47:13Z
body: I ran into the floating point comparison problem on macos when implementing this change for `gdalraster::pixel_extract()` yesterday. Tests using byte.tif at the right edge and bottom right corner failed in CI only on macos:
https://github.com/USDAForestService/gdalraster/actions/runs/14275628853/job/40020619427

FWIW, I used `ARE_REAL_EQUAL()` like below. I could not see any performance impact when extracting over large numbers of points. It passes checks on all the platforms at https://github.com/USDAForestService/gdalraster/actions/runs/14278208416.

```cpp
//...
            double grid_x = inv_gt[0] + inv_gt[1] * geo_x + inv_gt[2] * geo_y;
            double grid_y = inv_gt[3] + inv_gt[4] * geo_x + inv_gt[5] * geo_y;

            if ((grid_x < 0 || grid_x > static_cast<double>(raster_xsize) ||
                 grid_y < 0 || grid_y > static_cast<double>(raster_ysize)) &&
                !(ARE_REAL_EQUAL(grid_x, static_cast<double>(raster_xsize)) ||
                  ARE_REAL_EQUAL(grid_y, static_cast<double>(raster_ysize)))) {

                if (band_idx == 0)
                    pts_outside += 1;

                values.row(row_idx) = Rcpp::NumericVector(values.ncol(),
                                                          NA_REAL);
                continue;
            }

            if (eResampleAlg == GRIORA_NearestNeighbour && krnl_dim == 1) {
                // allow input coordinates exactly on the bottom or right edges
                // match behavior in: https://github.com/OSGeo/gdal/pull/12080
                if (ARE_REAL_EQUAL(grid_x, static_cast<double>(raster_xsize)))
                    grid_x -= 0.25;
                if (ARE_REAL_EQUAL(grid_y, static_cast<double>(raster_ysize)))
                    grid_y -= 0.25;

//...
```
author_association: CONTRIBUTOR
user: login: rouault
created_at: 2025-04-07T11:36:00Z
body: @dbaston Can you remerge https://github.com/OSGeo/gdal/pull/12080 into this branch ?
author_association: MEMBER
user: login: dbaston
created_at: 2025-04-08T18:04:26Z
body: > @dbaston Can you remerge #12080 into this branch ?

done
author_association: MEMBER