Fix PixelIsPoint overview coord offset (#1642)#1645
Merged
Conversation
PR #1641 (issue #1640) inherits level-0 georef on overview reads but keeps the level-0 origin_x / origin_y unchanged. That is correct for PixelIsArea -- origin is the upper-left corner of pixel (0, 0), which is also the upper-left corner of the overview's pixel (0, 0). It is wrong for PixelIsPoint (GeoKey 1025 = 2): the origin is the center of pixel (0, 0), and an overview pixel covering the first scale_x columns of level 0 has its center at the centroid of those level-0 pixels (origin + (scale - 1) * 0.5 * pixel_size_lvl0). Before this fix, open_geotiff on a 1024x1024 PixelIsPoint COG with 10 m pixels and origin (0, 0) returned x[:3] = [0, 20, 40] for overview_level=1 instead of [5, 25, 45]. Downstream sel / interp / reproject silently snaps to the wrong pixel for any DEM-style PixelIsPoint COG (USGS, OpenTopography, Copernicus DEM). Fix in extract_geo_info_with_overview_inheritance: choose the effective raster_type first (overview's own when it explicitly declared non-default, otherwise inherit from level 0), then apply origin_shift = (scale - 1) * 0.5 * pixel_size_lvl0 along each axis when that effective raster_type is PixelIsPoint. The PixelIsArea path is byte-equivalent to before. Add 13 regression tests in test_overview_pixel_is_point_1642.py: centroid identity across all four backends, transform-tuple values across all four backends, uniform grid step, unit-level helper tests for both raster_types via stubbed extract_geo_info, an own-geokeys- not-clobbered case on PixelIsPoint, and a PixelIsArea regression check so the #1640 contract still holds. All 1397 existing non-network geotiff tests still pass.
Contributor
There was a problem hiding this comment.
Pull request overview
Fixes GeoTIFF overview georeferencing for raster_type=PixelIsPoint by adjusting the inherited origin so overview pixel centers align with the centroid of the level-0 pixels they aggregate (closing #1642). This keeps the PixelIsArea behavior unchanged while making overview reads accurate for DEM-style PixelIsPoint COGs.
Changes:
- Update
extract_geo_info_with_overview_inheritanceto apply a(scale - 1) * 0.5 * pixel_size_lvl0origin shift when the effective raster type isPixelIsPoint. - Add comprehensive regression tests covering all four read backends and both raster-type paths.
- Update
.claude/sweep-accuracy-state.csvto record the sweep result for #1642.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
xrspatial/geotiff/_geotags.py |
Applies a raster-type-dependent origin shift when inheriting level-0 georef for overview IFDs. |
xrspatial/geotiff/tests/test_overview_pixel_is_point_1642.py |
New end-to-end + unit-level regression tests validating correct PixelIsPoint overview coords/transform across backends. |
.claude/sweep-accuracy-state.csv |
Records the sweep-accuracy pass note for issue #1642. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
extract_geo_info_with_overview_inheritancenow shifts the inheritedorigin by
(scale - 1) * 0.5 * pixel_size_lvl0along each axis whenthe effective
raster_typeisPixelIsPoint, so an overview pixel'sreported center matches the centroid of the level-0 pixels it covers.
PixelIsAreapath is byte-equivalent to before -- origin stays atthe level-0 upper-left corner. The geotiff: open_geotiff(overview_level>=1) drops CRS, transform, and georeferenced coords #1640 contract is unchanged for
the default convention.
Why
PR #1641 (issue #1640) made overview reads inherit the level-0
GeoKeys / ModelPixelScale / ModelTiepoint, but it kept
origin_xandorigin_yunchanged. The defaultPixelIsAreaconvention says theorigin is the upper-left corner of pixel (0, 0), so that's correct
there.
PixelIsPoint(GeoKey 1025 = 2) says the origin is thecenter of pixel (0, 0). An overview pixel covering the first
scale_xcolumns of level 0 has its center at the centroid of thoselevel-0 pixels, so the inherited origin needs a shift.
The bug silently snapped coords by half an overview pixel on every
DEM-style PixelIsPoint COG (USGS, OpenTopography, Copernicus DEM all
emit
RasterPixelIsPoint).da.sel,da.interp, and downstreamreproject/hillshade/slope chains pick up the wrong pixel position
without raising.
Before / after on a 1024x1024 PixelIsPoint COG with 10 m pixels and
origin (0, 0):
Test plan
python -m pytest xrspatial/geotiff/tests/test_overview_pixel_is_point_1642.py(13 passed)python -m pytest xrspatial/geotiff/tests/test_overview_geo_inheritance_1640.py(15 passed; geotiff: open_geotiff(overview_level>=1) drops CRS, transform, and georeferenced coords #1640 contract intact)python -m pytest xrspatial/geotiff/tests/ -k "not http and not network"(1397 passed; 3 pre-existing matplotlib palette failures unrelated)Notes
The 13 new tests cover all four backends (numpy, dask+numpy, cupy,
dask+cupy) for both
PixelIsPointand aPixelIsArearegressioncheck, plus three unit-level helper tests via stubbed
extract_geo_infothat exercise the math without going through thewriter/reader pipeline.
Found by /sweep-accuracy pass 18 on the geotiff module.