Skip to content

Fix _coords_to_transform for 3D (y,x,band) DataArrays (#1643)#1648

Merged
brendancol merged 2 commits into
mainfrom
issue-1643
May 12, 2026
Merged

Fix _coords_to_transform for 3D (y,x,band) DataArrays (#1643)#1648
brendancol merged 2 commits into
mainfrom
issue-1643

Conversation

@brendancol
Copy link
Copy Markdown
Contributor

Summary

Closes #1643.

_coords_to_transform looked up y/x coords via dims[-2] / dims[-1]. On a 3D (y, x, band) DataArray that picked x and band instead of y and x, so to_geotiff and write_geotiff_gpu silently emitted a wrong GeoTransform whenever the fallback path ran (i.e. attrs['transform'] was absent). The bug surfaced as a round-tripped file with pixel_width=1.0 (the band axis spacing) and pixel coords offset by ~hundreds of metres on real rasters.

The fix detects the band-like dim ('band', 'bands', 'channel') and uses the two remaining spatial dims. 2D arrays keep their original dims[-2:] behaviour. Both (y, x, band) and (band, y, x) 3D layouts now resolve to the y/x transform.

Test plan

  • New regression tests in xrspatial/geotiff/tests/test_coords_to_transform_3d_1643.py cover:
    • direct helper behaviour for (y, x, band) and (band, y, x) and 2D inputs
    • end-to-end to_geotiff + open_geotiff round-trip parity between 2D and 3D
    • GPU writer (write_geotiff_gpu) round-trip on CUDA hosts
  • Full geotiff test suite still passes (1398 passed, 7 pre-existing matplotlib TestPalette failures unrelated to this PR).

_coords_to_transform read y/x coords via dims[-2:] which on a 3D
(y, x, band) DataArray picked (x, band) instead of (y, x). to_geotiff
and write_geotiff_gpu silently emitted a wrong GeoTransform on the
fallback path when attrs['transform'] was absent (the round-tripped
file used the band axis spacing as pixel_width).

The helper now skips any trailing/leading dim named band/bands/channel
and uses the two remaining spatial dims. 2D inputs and 3D (band, y, x)
inputs are both handled.
@github-actions github-actions Bot added the performance PR touches performance-sensitive code label May 12, 2026
@brendancol brendancol requested a review from Copilot May 12, 2026 00:37
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes incorrect GeoTransform inference for 3D GeoTIFF-oriented xr.DataArrays by making _coords_to_transform select spatial y/x dims even when a band-like dim is present (e.g. (y, x, band) / (band, y, x)), preventing silently wrong transforms in to_geotiff / write_geotiff_gpu fallback paths.

Changes:

  • Update _coords_to_transform to ignore band-like dims (band / bands / channel) when inferring the transform for 3D arrays.
  • Add regression tests covering _coords_to_transform behavior and CPU/GPU round-trip parity for 3D inputs without attrs['transform'].

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
xrspatial/geotiff/__init__.py Adjusts _coords_to_transform to choose the two non-band dims for 3D arrays and documents the rationale.
xrspatial/geotiff/tests/test_coords_to_transform_3d_1643.py Adds regression coverage for direct helper behavior and end-to-end GeoTIFF round-trips for 3D layouts (including GPU path).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +12 to +14
import os
import tempfile

Comment on lines +21 to +27
try:
import cupy # noqa: F401
HAS_CUPY = True
except ImportError:
HAS_CUPY = False


Comment on lines +47 to +64
def test_coords_to_transform_yxband_returns_yx_spacing():
"""3D (y, x, band) picks y/x spacing rather than (x, band) spacing."""
da = _make_geo_da_3d(('y', 'x', 'band'))
gt = _coords_to_transform(da)
# y spacing = (200 - 100) / 9, x spacing = (700 - 500) / 19
assert gt is not None
np.testing.assert_allclose(gt.pixel_width, (700.0 - 500.0) / 19)
np.testing.assert_allclose(gt.pixel_height, (200.0 - 100.0) / 9)


def test_coords_to_transform_bandyx_returns_yx_spacing():
"""3D (band, y, x) also returns the y/x transform."""
da = _make_geo_da_3d(('band', 'y', 'x'))
gt = _coords_to_transform(da)
assert gt is not None
np.testing.assert_allclose(gt.pixel_width, (700.0 - 500.0) / 19)
np.testing.assert_allclose(gt.pixel_height, (200.0 - 100.0) / 9)

Comment thread xrspatial/geotiff/__init__.py Outdated
Comment on lines +196 to +203
skips any trailing/leading dim named ``band`` / ``bands`` / ``channel``
so a ``(y, x, band)`` or ``(band, y, x)`` DataArray returns the y/x
transform rather than picking up the band axis spacing as a pixel
size. ``to_geotiff`` itself remaps ``(band, y, x)`` arrays to
``(y, x, band)`` before writing pixel bytes, but it calls
:func:`_coords_to_transform` against the original DataArray, so the
helper must handle both layouts to keep the geo-transform consistent
with the file's coord arrays. See issue #1643.
Comment thread xrspatial/geotiff/__init__.py Outdated
Comment on lines +205 to +213
_BAND_DIM_NAMES = ('band', 'bands', 'channel')
if da.ndim == 3:
# Drop the band-like dim and keep the two spatial dims in their
# original (y, x) order. Position-based fallback covers the case
# where none of the dims are named like a band axis.
spatial = tuple(d for d in da.dims if d not in _BAND_DIM_NAMES)
if len(spatial) == 2:
ydim, xdim = spatial[0], spatial[1]
else:
- Lift _BAND_DIM_NAMES to module scope and reuse at the three (band,y,x)
  remap sites in __init__.py to avoid drift between _coords_to_transform
  and the writer paths.
- Reword _coords_to_transform docstring: filter is position-independent,
  not trailing/leading.
- Drop unused os/tempfile imports from the regression test.
- Replace `import cupy` guard with the repo's standard _gpu_available()
  pattern that also checks `cupy.cuda.is_available()` and swallows
  non-ImportError import failures.
- Add parametrized helper coverage for 'bands' and 'channel' dim names.
@brendancol brendancol merged commit babb72e into main May 12, 2026
10 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance PR touches performance-sensitive code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

geotiff: to_geotiff silently writes a wrong GeoTransform for 3D (y,x,band) arrays missing attrs['transform']

2 participants