Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .claude/sweep-metadata-state.csv
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
module,last_inspected,issue,severity_max,categories_found,notes
geotiff,2026-05-12,1710,MEDIUM,2,"open_geotiff/read_geotiff_dask/read_geotiff_gpu windowed reads of non-georef TIFFs produced float64 half-pixel-shifted coords while full reads produced int64 [0,1,2,...] coords. Affected every backend the same way; not a backend parity bug, a windowed-vs-full inconsistency. _populate_attrs_from_geo_info also fabricated an identity transform attr on non-georef files. Fixed by threading has_georef through all windowed coord paths and through the transform attr emitter (#1710)."
geotiff,2026-05-12,1739,HIGH,1;4,"COG overview reads dropped attrs['nodata'] from level 0, so the writer-baked sentinel survived as raw data in the overview pixels (silent numerical corruption). extract_geo_info_with_overview_inheritance was inheriting CRS-side fields only; extended to per-IFD pass-through tags (nodata, gdal_metadata*, resolution*, colormap, extra_tags, image_description, extra_samples). All four backends affected (numpy/dask/cupy/dask+cupy). Fixed in #1739."
geotiff,2026-05-12,1753,HIGH,2,"read_geotiff_gpu stripped-fallback windowed read on a non-georef TIFF emitted float64 [-0.5, -1.5, ...] coords via the default unit GeoTransform placeholder, while the eager numpy and dask paths emit int64 file-relative pixel coords. Regression of #1710 (fix missed the stripped GPU branch). The tiled-GPU helper _gpu_apply_window_band already gates on has_georef correctly; the stripped fallback only checked t is None. Fixed by routing both non-georef and t-is-None cases through the integer pixel-coord branch and using file-relative offsets to match every other backend. Verified 4-backend parity (numpy / cupy / dask+numpy / dask+cupy)."
reproject,2026-05-10,1572;1573,HIGH,1;3;4,geoid_height_raster dropped input attrs and used dims[-2:] for 3D inputs (#1572). reproject/merge ignored nodatavals (rasterio convention) when rioxarray absent (#1573). Fixed in same branch.
15 changes: 11 additions & 4 deletions xrspatial/geotiff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2720,14 +2720,21 @@ def read_geotiff_gpu(source: str, *,
arr_gpu = arr_gpu.astype(target)
# ``read_to_array`` already applied window + band slicing, so
# ``arr_gpu`` is at output shape. Compute coords for that
# shape without re-slicing.
# shape without re-slicing. Mirror the eager-numpy /
# ``read_geotiff_dask`` / ``_gpu_apply_window_band`` checks
# against ``has_georef``: a non-georef TIFF carries a
# default ``GeoTransform()`` placeholder (``t is None`` is
# never true here) so a transform-based coord path would
# emit synthetic ``[-0.5, -1.5, ...]`` floats instead of
# the integer pixel coords every other backend produces
# (#1753 / regression of #1710).
if window is not None:
r0, c0, r1, c1 = window
t = geo_info.transform
if t is None:
if t is None or not getattr(geo_info, 'has_georef', True):
coords = {
'y': np.arange(r1 - r0, dtype=np.int64),
'x': np.arange(c1 - c0, dtype=np.int64),
'y': np.arange(r0, r1, dtype=np.int64),
'x': np.arange(c0, c1, dtype=np.int64),
}
elif geo_info.raster_type == RASTER_PIXEL_IS_POINT:
coords = {
Expand Down
28 changes: 28 additions & 0 deletions xrspatial/geotiff/tests/test_no_georef_windowed_coords_1710.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,34 @@ def test_dask_cupy_windowed_integer_coords(self, no_georef_path_1710):
assert da.y.dtype == np.int64
np.testing.assert_array_equal(da.y.values, np.arange(4))

def test_offset_window_integer_coords(self, no_georef_path_1710):
"""GPU windowed read at a non-zero origin: the stripped-GPU
fallback in ``read_geotiff_gpu`` checked ``t is None`` instead
of ``has_georef`` (issue #1753 / regression of #1710), so a
non-georef TIFF emitted ``[-0.5, -1.5, ...]`` via the unit
``GeoTransform`` placeholder. Pin the contract that the offset
window produces file-relative integer coords identical to the
eager numpy path.
"""
da = open_geotiff(no_georef_path_1710, gpu=True,
window=(2, 3, 6, 7))
assert da.y.dtype == np.int64
assert da.x.dtype == np.int64
np.testing.assert_array_equal(da.y.values, np.arange(2, 6))
np.testing.assert_array_equal(da.x.values, np.arange(3, 7))

def test_offset_window_no_transform_attr(self, no_georef_path_1710):
"""Non-georef GPU windowed read still must not advertise a
fabricated ``attrs['transform']`` -- ``_populate_attrs_from_geo_info``
gates on ``has_georef`` too, so flag the round-trip here.
"""
da = open_geotiff(no_georef_path_1710, gpu=True,
window=(2, 3, 6, 7))
assert 'transform' not in da.attrs, (
f"non-georef GPU windowed read should not carry a "
f"fabricated transform; got attrs={dict(da.attrs)}"
)


class TestBackendParity:
"""Full read and windowed read must agree on coord dtype and values
Expand Down
Loading