Skip to content

geotiff: to_geotiff loses sign of pixel scale for descending x or ascending y #1716

@brendancol

Description

@brendancol

Describe the bug

to_geotiff infers signed pixel sizes from coords in _coords_to_transform (xrspatial/geotiff/init.py:302-303) but the GeoTIFF writer at xrspatial/geotiff/_geotags.py:891-894 stores abs(transform.pixel_width) and abs(transform.pixel_height) in ModelPixelScaleTag. The reader at xrspatial/geotiff/_geotags.py:457-461 then reconstructs the transform as pixel_width=sx, pixel_height=-sy, hard-coding the conventional north-up / west-east orientation.

The result: any DataArray whose x decreases left-to-right or whose y increases top-to-bottom round-trips with the wrong georeferencing. The pixel data is unchanged, so the file still looks fine in a viewer, but every coord-aware operation downstream is offset.

Expected behavior

Round-tripping a DataArray through to_geotiff + open_geotiff should preserve the coordinate orientation. Concretely:

```python
import numpy as np
import xarray as xr
from xrspatial.geotiff import to_geotiff, open_geotiff

arr = xr.DataArray(
np.arange(6, dtype=np.float32).reshape(2, 3),
dims=('y', 'x'),
coords={'x': [100.0, 99.0, 98.0], 'y': [10.0, 11.0]},
)
to_geotiff(arr, 'tmp.tif')
out = open_geotiff('tmp.tif')

Today: out.coords['x'] == [101, 102, 103], out.coords['y'] == [9, 8]

Expected: same as input coords

```

Proposed fix

ModelPixelScaleTag is defined as a positive-only scale; the standard GeoTIFF way to encode non-standard orientations is ModelTransformationTag (33920), which stores the full 4x4 affine and supersedes the pixel-scale+tiepoint pair when present. So:

  1. In build_geotiff_tags, emit ModelTransformationTag instead of ModelPixelScale + ModelTiepoint whenever pixel_width < 0 or pixel_height > 0.
  2. In the reader, read ModelTransformationTag when present and prefer it over the scale+tiepoint reconstruction. Continue to fall back to scale+tiepoint for the common north-up case.

This keeps standard-orientation files byte-identical to before and only changes output for the descending-x / ascending-y paths that are currently silently broken.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions