Description
A TIFF with no GeoTIFF tags (no ModelPixelScale, no ModelTiepoint, no GeoKeys) gets different y/x coordinates depending on whether window= is passed.
Full read takes the has_georef=False branch in _geo_to_coords and returns integer pixel coords [0, 1, 2, ...] with int64 dtype.
Windowed read of the same file skips _geo_to_coords entirely and synthesises coords from the default GeoTransform(origin=0, pixel_width=1, pixel_height=-1). That produces float64 coords like [-0.5, -1.5, -2.5, ...] because the PixelIsArea half-pixel shift gets applied to a unit transform that was never real.
Every read path has the same split: open_geotiff, read_geotiff_dask, read_geotiff_gpu, and dask+cupy.
Reproduction
import os, tempfile
import numpy as np
from xrspatial.geotiff import open_geotiff
from xrspatial.geotiff._writer import write
with tempfile.TemporaryDirectory() as tmp:
arr = np.arange(64, dtype=np.float32).reshape(8, 8)
path = os.path.join(tmp, "no_georef.tif")
write(arr, path, compression="none", tiled=False)
full = open_geotiff(path)
win = open_geotiff(path, window=(0, 0, 4, 4))
print(full.y.dtype, full.y.values[:4]) # int64 [0 1 2 3]
print(win.y.dtype, win.y.values) # float64 [-0.5 -1.5 -2.5 -3.5]
Expected
Windowed reads of a non-georef TIFF should match the full-read convention: integer pixel indices [r0, r0+1, ..., r1-1] for y and [c0, c0+1, ..., c1-1] for x, dtype int64.
The DataArray also should not carry a synthetic attrs['transform'] for a file with no GeoTIFF tags. A non-georef file should round-trip through to_geotiff without picking up a fabricated identity transform.
Affected code
xrspatial/geotiff/__init__.py open_geotiff lines 695-707 (eager numpy windowed coord)
xrspatial/geotiff/__init__.py read_geotiff_dask lines 1839-1864 (dask windowed coord)
xrspatial/geotiff/__init__.py _gpu_apply_window_band lines 2271-2298 (GPU windowed coord)
xrspatial/geotiff/__init__.py _populate_attrs_from_geo_info lines 438-451 (transform attr emission)
Severity
MEDIUM. Only triggers for TIFFs without GeoTIFF tags, which are rare in spatial workflows. But the silent int64-to-float64 shift and the half-pixel coord offset between full and windowed reads of the same file can quietly break downstream coord-based math.
Found by
Metadata propagation sweep, 2026-05-12.
Description
A TIFF with no GeoTIFF tags (no
ModelPixelScale, noModelTiepoint, no GeoKeys) gets differenty/xcoordinates depending on whetherwindow=is passed.Full read takes the
has_georef=Falsebranch in_geo_to_coordsand returns integer pixel coords[0, 1, 2, ...]withint64dtype.Windowed read of the same file skips
_geo_to_coordsentirely and synthesises coords from the defaultGeoTransform(origin=0, pixel_width=1, pixel_height=-1). That produces float64 coords like[-0.5, -1.5, -2.5, ...]because thePixelIsAreahalf-pixel shift gets applied to a unit transform that was never real.Every read path has the same split:
open_geotiff,read_geotiff_dask,read_geotiff_gpu, and dask+cupy.Reproduction
Expected
Windowed reads of a non-georef TIFF should match the full-read convention: integer pixel indices
[r0, r0+1, ..., r1-1]for y and[c0, c0+1, ..., c1-1]for x, dtypeint64.The DataArray also should not carry a synthetic
attrs['transform']for a file with no GeoTIFF tags. A non-georef file should round-trip throughto_geotiffwithout picking up a fabricated identity transform.Affected code
xrspatial/geotiff/__init__.pyopen_geotifflines 695-707 (eager numpy windowed coord)xrspatial/geotiff/__init__.pyread_geotiff_dasklines 1839-1864 (dask windowed coord)xrspatial/geotiff/__init__.py_gpu_apply_window_bandlines 2271-2298 (GPU windowed coord)xrspatial/geotiff/__init__.py_populate_attrs_from_geo_infolines 438-451 (transform attr emission)Severity
MEDIUM. Only triggers for TIFFs without GeoTIFF tags, which are rare in spatial workflows. But the silent int64-to-float64 shift and the half-pixel coord offset between full and windowed reads of the same file can quietly break downstream coord-based math.
Found by
Metadata propagation sweep, 2026-05-12.