Skip to content

Commit 015daca

Browse files
authored
Use rasterio's transform instead of homemade coordinates (#1712)
1 parent 0092911 commit 015daca

File tree

3 files changed

+135
-27
lines changed

3 files changed

+135
-27
lines changed

doc/io.rst

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -512,10 +512,14 @@ rasterio is installed. Here is an example of how to use
512512
[1703814 values with dtype=uint8]
513513
Coordinates:
514514
* band (band) int64 1 2 3
515-
* y (y) float64 2.827e+06 2.827e+06 2.826e+06 2.826e+06 2.826e+06 ...
516-
* x (x) float64 1.02e+05 1.023e+05 1.026e+05 1.029e+05 1.032e+05 ...
515+
* y (y) float64 2.827e+06 2.826e+06 2.826e+06 2.826e+06 2.826e+06 ...
516+
* x (x) float64 1.021e+05 1.024e+05 1.027e+05 1.03e+05 1.033e+05 ...
517517
Attributes:
518-
crs: +init=epsg:32618
518+
res: (300.0379266750948, 300.041782729805)
519+
transform: (300.0379266750948, 0.0, 101985.0, 0.0, -300.041782729805, 28...
520+
is_tiled: 0
521+
crs: +init=epsg:32618
522+
519523

520524
The ``x`` and ``y`` coordinates are generated out of the file's metadata
521525
(``bounds``, ``width``, ``height``), and they can be understood as cartesian

xarray/backends/rasterio_.py

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import os
2+
import warnings
23
from collections import OrderedDict
4+
from distutils.version import LooseVersion
35
import numpy as np
46

57
from .. import DataArray
@@ -113,7 +115,8 @@ def default(s):
113115
return parsed_meta
114116

115117

116-
def open_rasterio(filename, chunks=None, cache=None, lock=None):
118+
def open_rasterio(filename, parse_coordinates=None, chunks=None, cache=None,
119+
lock=None):
117120
"""Open a file with rasterio (experimental).
118121
119122
This should work with any file that rasterio can open (most often:
@@ -123,15 +126,25 @@ def open_rasterio(filename, chunks=None, cache=None, lock=None):
123126
<http://web.archive.org/web/20160326194152/http://remotesensing.org/geotiff/spec/geotiff2.5.html#2.5.2>`_
124127
for more information).
125128
129+
You can generate 2D coordinates from the file's attributes with::
130+
131+
from affine import Affine
132+
da = xr.open_rasterio('path_to_file.tif')
133+
transform = Affine(*da.attrs['transform'])
134+
nx, ny = da.sizes['x'], da.sizes['y']
135+
x, y = np.meshgrid(np.arange(nx)+0.5, np.arange(ny)+0.5) * transform
136+
137+
126138
Parameters
127139
----------
128140
filename : str
129141
Path to the file to open.
130-
131-
Returns
132-
-------
133-
data : DataArray
134-
The newly created DataArray.
142+
parse_coordinates : bool, optional
143+
Whether to parse the x and y coordinates out of the file's
144+
``transform`` attribute or not. The default is to automatically
145+
parse the coordinates only if they are rectilinear (1D).
146+
It can be useful to set ``parse_coordinates=False``
147+
if your files are very large or if you don't need the coordinates.
135148
chunks : int, tuple or dict, optional
136149
Chunk sizes along each dimension, e.g., ``5``, ``(5, 5)`` or
137150
``{'x': 5, 'y': 5}``. If chunks is provided, it used to load the new
@@ -146,6 +159,11 @@ def open_rasterio(filename, chunks=None, cache=None, lock=None):
146159
:py:func:`dask.array.from_array`. By default, a global lock is
147160
used to avoid issues with concurrent access to the same file when using
148161
dask's multithreaded backend.
162+
163+
Returns
164+
-------
165+
data : DataArray
166+
The newly created DataArray.
149167
"""
150168

151169
import rasterio
@@ -161,18 +179,38 @@ def open_rasterio(filename, chunks=None, cache=None, lock=None):
161179
raise ValueError('Unknown dims')
162180
coords['band'] = np.asarray(riods.indexes)
163181

164-
# Get geo coords
165-
nx, ny = riods.width, riods.height
166-
dx, dy = riods.res[0], -riods.res[1]
167-
x0 = riods.bounds.right if dx < 0 else riods.bounds.left
168-
y0 = riods.bounds.top if dy < 0 else riods.bounds.bottom
169-
coords['y'] = np.linspace(start=y0 + dy / 2, num=ny,
170-
stop=(y0 + (ny - 1) * dy) + dy / 2)
171-
coords['x'] = np.linspace(start=x0 + dx / 2, num=nx,
172-
stop=(x0 + (nx - 1) * dx) + dx / 2)
182+
# Get coordinates
183+
if LooseVersion(rasterio.__version__) < '1.0':
184+
transform = riods.affine
185+
else:
186+
transform = riods.transform
187+
if transform.is_rectilinear:
188+
# 1d coordinates
189+
parse = True if parse_coordinates is None else parse_coordinates
190+
if parse:
191+
nx, ny = riods.width, riods.height
192+
# xarray coordinates are pixel centered
193+
x, _ = (np.arange(nx)+0.5, np.zeros(nx)+0.5) * transform
194+
_, y = (np.zeros(ny)+0.5, np.arange(ny)+0.5) * transform
195+
coords['y'] = y
196+
coords['x'] = x
197+
else:
198+
# 2d coordinates
199+
parse = False if (parse_coordinates is None) else parse_coordinates
200+
if parse:
201+
warnings.warn("The file coordinates' transformation isn't "
202+
"rectilinear: xarray won't parse the coordinates "
203+
"in this case. Set `parse_coordinates=False` to "
204+
"suppress this warning.",
205+
RuntimeWarning, stacklevel=3)
173206

174207
# Attributes
175-
attrs = {}
208+
attrs = dict()
209+
# Affine transformation matrix (always available)
210+
# This describes coefficients mapping pixel coordinates to CRS
211+
# For serialization store as tuple of 6 floats, the last row being
212+
# always (0, 0, 1) per definition (see https://github.com/sgillies/affine)
213+
attrs['transform'] = tuple(transform)[:6]
176214
if hasattr(riods, 'crs') and riods.crs:
177215
# CRS is a dict-like object specific to rasterio
178216
# If CRS is not None, we convert it back to a PROJ4 string using

xarray/tests/test_backends.py

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2227,30 +2227,96 @@ def test_utm(self):
22272227
with create_tmp_geotiff() as (tmp_file, expected):
22282228
with xr.open_rasterio(tmp_file) as rioda:
22292229
assert_allclose(rioda, expected)
2230-
assert 'crs' in rioda.attrs
22312230
assert isinstance(rioda.attrs['crs'], basestring)
2232-
assert 'res' in rioda.attrs
22332231
assert isinstance(rioda.attrs['res'], tuple)
2234-
assert 'is_tiled' in rioda.attrs
22352232
assert isinstance(rioda.attrs['is_tiled'], np.uint8)
2236-
assert 'transform' in rioda.attrs
22372233
assert isinstance(rioda.attrs['transform'], tuple)
22382234

2235+
# Check no parse coords
2236+
with xr.open_rasterio(tmp_file, parse_coordinates=False) as rioda:
2237+
assert 'x' not in rioda.coords
2238+
assert 'y' not in rioda.coords
2239+
2240+
def test_non_rectilinear(self):
2241+
import rasterio
2242+
from rasterio.transform import from_origin
2243+
2244+
# Create a geotiff file with 2d coordinates
2245+
with create_tmp_file(suffix='.tif') as tmp_file:
2246+
# data
2247+
nx, ny, nz = 4, 3, 3
2248+
data = np.arange(nx*ny*nz,
2249+
dtype=rasterio.float32).reshape(nz, ny, nx)
2250+
transform = from_origin(0, 3, 1, 1).rotation(45)
2251+
with rasterio.open(
2252+
tmp_file, 'w',
2253+
driver='GTiff', height=ny, width=nx, count=nz,
2254+
transform=transform,
2255+
dtype=rasterio.float32) as s:
2256+
s.write(data)
2257+
2258+
# Default is to not parse coords
2259+
with xr.open_rasterio(tmp_file) as rioda:
2260+
assert 'x' not in rioda.coords
2261+
assert 'y' not in rioda.coords
2262+
assert 'crs' not in rioda.attrs
2263+
assert isinstance(rioda.attrs['res'], tuple)
2264+
assert isinstance(rioda.attrs['is_tiled'], np.uint8)
2265+
assert isinstance(rioda.attrs['transform'], tuple)
2266+
2267+
# See if a warning is raised if we force it
2268+
with self.assertWarns("transformation isn't rectilinear"):
2269+
with xr.open_rasterio(tmp_file,
2270+
parse_coordinates=True) as rioda:
2271+
assert 'x' not in rioda.coords
2272+
assert 'y' not in rioda.coords
2273+
22392274
def test_platecarree(self):
22402275
with create_tmp_geotiff(8, 10, 1, transform_args=[1, 2, 0.5, 2.],
22412276
crs='+proj=latlong') \
22422277
as (tmp_file, expected):
22432278
with xr.open_rasterio(tmp_file) as rioda:
22442279
assert_allclose(rioda, expected)
2245-
assert 'crs' in rioda.attrs
22462280
assert isinstance(rioda.attrs['crs'], basestring)
2247-
assert 'res' in rioda.attrs
22482281
assert isinstance(rioda.attrs['res'], tuple)
2249-
assert 'is_tiled' in rioda.attrs
22502282
assert isinstance(rioda.attrs['is_tiled'], np.uint8)
2251-
assert 'transform' in rioda.attrs
22522283
assert isinstance(rioda.attrs['transform'], tuple)
22532284

2285+
def test_notransform(self):
2286+
# regression test for https://github.com/pydata/xarray/issues/1686
2287+
import rasterio
2288+
import warnings
2289+
2290+
# Create a geotiff file
2291+
with warnings.catch_warnings():
2292+
# rasterio throws a NotGeoreferencedWarning here, which is
2293+
# expected since we test rasterio's defaults in this case.
2294+
warnings.filterwarnings('ignore', category=UserWarning,
2295+
message='Dataset has no geotransform set')
2296+
with create_tmp_file(suffix='.tif') as tmp_file:
2297+
# data
2298+
nx, ny, nz = 4, 3, 3
2299+
data = np.arange(nx*ny*nz,
2300+
dtype=rasterio.float32).reshape(nz, ny, nx)
2301+
with rasterio.open(
2302+
tmp_file, 'w',
2303+
driver='GTiff', height=ny, width=nx, count=nz,
2304+
dtype=rasterio.float32) as s:
2305+
s.write(data)
2306+
2307+
# Tests
2308+
expected = DataArray(data,
2309+
dims=('band', 'y', 'x'),
2310+
coords={'band': [1, 2, 3],
2311+
'y': [0.5, 1.5, 2.5],
2312+
'x': [0.5, 1.5, 2.5, 3.5],
2313+
})
2314+
with xr.open_rasterio(tmp_file) as rioda:
2315+
assert_allclose(rioda, expected)
2316+
assert isinstance(rioda.attrs['res'], tuple)
2317+
assert isinstance(rioda.attrs['is_tiled'], np.uint8)
2318+
assert isinstance(rioda.attrs['transform'], tuple)
2319+
22542320
def test_indexing(self):
22552321
with create_tmp_geotiff(8, 10, 3, transform_args=[1, 2, 0.5, 2.],
22562322
crs='+proj=latlong') as (tmp_file, expected):

0 commit comments

Comments
 (0)