rasterix 0.1a3__py3-none-any.whl → 0.1a4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rasterix/_version.py +2 -2
- rasterix/raster_index.py +327 -164
- rasterix/rasterize/rasterio.py +4 -3
- rasterix/utils.py +67 -0
- {rasterix-0.1a3.dist-info → rasterix-0.1a4.dist-info}/METADATA +7 -33
- rasterix-0.1a4.dist-info/RECORD +14 -0
- rasterix-0.1a3.dist-info/RECORD +0 -13
- {rasterix-0.1a3.dist-info → rasterix-0.1a4.dist-info}/WHEEL +0 -0
- {rasterix-0.1a3.dist-info → rasterix-0.1a4.dist-info}/licenses/LICENSE +0 -0
rasterix/_version.py
CHANGED
rasterix/raster_index.py
CHANGED
|
@@ -3,59 +3,97 @@ from __future__ import annotations
|
|
|
3
3
|
import math
|
|
4
4
|
import textwrap
|
|
5
5
|
from collections.abc import Hashable, Iterable, Mapping, Sequence
|
|
6
|
-
from typing import Any, Self, TypeVar
|
|
6
|
+
from typing import Any, Self, TypeVar, cast
|
|
7
7
|
|
|
8
8
|
import numpy as np
|
|
9
9
|
import pandas as pd
|
|
10
|
+
import xproj
|
|
10
11
|
from affine import Affine
|
|
11
|
-
from
|
|
12
|
+
from pyproj import CRS
|
|
13
|
+
from xarray import Coordinates, DataArray, Dataset, Index, Variable, get_options
|
|
12
14
|
from xarray.core.coordinate_transform import CoordinateTransform
|
|
13
15
|
|
|
14
16
|
# TODO: import from public API once it is available
|
|
15
|
-
from xarray.core.indexes import CoordinateTransformIndex
|
|
17
|
+
from xarray.core.indexes import CoordinateTransformIndex
|
|
16
18
|
from xarray.core.indexing import IndexSelResult, merge_sel_results
|
|
19
|
+
from xarray.core.types import JoinOptions
|
|
20
|
+
from xproj.typing import CRSAwareIndex
|
|
17
21
|
|
|
18
22
|
from rasterix.odc_compat import BoundingBox, bbox_intersection, bbox_union, maybe_int, snap_grid
|
|
19
23
|
from rasterix.rioxarray_compat import guess_dims
|
|
24
|
+
from rasterix.utils import get_affine
|
|
20
25
|
|
|
21
26
|
T_Xarray = TypeVar("T_Xarray", "DataArray", "Dataset")
|
|
22
27
|
|
|
23
28
|
__all__ = ["assign_index", "RasterIndex"]
|
|
24
29
|
|
|
25
30
|
|
|
26
|
-
|
|
31
|
+
# X/Y axis order conventions used for public and internal attributes
|
|
32
|
+
XAXIS = 0
|
|
33
|
+
YAXIS = 1
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def assign_index(
|
|
37
|
+
obj: T_Xarray, *, x_dim: str | None = None, y_dim: str | None = None, crs: bool = True
|
|
38
|
+
) -> T_Xarray:
|
|
27
39
|
"""Assign a RasterIndex to an Xarray DataArray or Dataset.
|
|
28
40
|
|
|
41
|
+
By default, the affine transform is guessed by first looking for a ``GeoTransform`` attribute
|
|
42
|
+
on a CF "grid mapping" variable (commonly ``"spatial_ref"``). If not present, then the affine is determined from 1D coordinate
|
|
43
|
+
variables named ``x_dim`` and ``y_dim`` provided to this function.
|
|
44
|
+
|
|
29
45
|
Parameters
|
|
30
46
|
----------
|
|
31
47
|
obj : xarray.DataArray or xarray.Dataset
|
|
32
|
-
The object to assign the index to.
|
|
48
|
+
The object to assign the index to.
|
|
33
49
|
x_dim : str, optional
|
|
34
50
|
Name of the x dimension. If None, will be automatically detected.
|
|
35
51
|
y_dim : str, optional
|
|
36
52
|
Name of the y dimension. If None, will be automatically detected.
|
|
53
|
+
crs: bool, optional
|
|
54
|
+
Auto-detect CRS using xproj?
|
|
37
55
|
|
|
38
56
|
Returns
|
|
39
57
|
-------
|
|
40
58
|
xarray.DataArray or xarray.Dataset
|
|
41
59
|
The input object with RasterIndex coordinates assigned.
|
|
42
60
|
|
|
61
|
+
Notes
|
|
62
|
+
-----
|
|
63
|
+
The "grid mapping" variable is determined following the CF conventions:
|
|
64
|
+
|
|
65
|
+
- If a DataArray is provided, we look for an attribute named ``"grid_mapping"``.
|
|
66
|
+
- For a Dataset, we pull the first detected ``"grid_mapping"`` attribute when iterating over data variables.
|
|
67
|
+
|
|
68
|
+
The value of this attribute is a variable name containing projection information (commonly ``"spatial_ref"``).
|
|
69
|
+
We then look for a ``"GeoTransform"`` attribute on this variable (following GDAL convention).
|
|
70
|
+
|
|
71
|
+
References
|
|
72
|
+
----------
|
|
73
|
+
- `CF conventions document <http://cfconventions.org/Data/cf-conventions/cf-conventions-1.11/cf-conventions.html#grid-mappings-and-projections>`_.
|
|
74
|
+
- `GDAL docs on GeoTransform <https://gdal.org/en/stable/tutorials/geotransforms_tut.html>`_.
|
|
75
|
+
|
|
43
76
|
Examples
|
|
44
77
|
--------
|
|
45
78
|
>>> import xarray as xr
|
|
46
|
-
>>> import rioxarray # Required for
|
|
79
|
+
>>> import rioxarray # Required for reading TIFF
|
|
47
80
|
>>> da = xr.open_dataset("path/to/raster.tif", engine="rasterio")
|
|
48
81
|
>>> indexed_da = assign_index(da)
|
|
49
82
|
"""
|
|
50
|
-
import rioxarray # noqa
|
|
51
|
-
|
|
52
83
|
if x_dim is None or y_dim is None:
|
|
53
84
|
guessed_x, guessed_y = guess_dims(obj)
|
|
54
85
|
x_dim = x_dim or guessed_x
|
|
55
86
|
y_dim = y_dim or guessed_y
|
|
56
87
|
|
|
88
|
+
affine = get_affine(obj, x_dim=x_dim, y_dim=y_dim, clear_transform=True)
|
|
89
|
+
|
|
57
90
|
index = RasterIndex.from_transform(
|
|
58
|
-
|
|
91
|
+
affine,
|
|
92
|
+
width=obj.sizes[x_dim],
|
|
93
|
+
height=obj.sizes[y_dim],
|
|
94
|
+
x_dim=x_dim,
|
|
95
|
+
y_dim=y_dim,
|
|
96
|
+
crs=obj.proj.crs if crs else None,
|
|
59
97
|
)
|
|
60
98
|
coords = Coordinates.from_xindex(index)
|
|
61
99
|
return obj.assign_coords(coords)
|
|
@@ -91,8 +129,8 @@ class AffineTransform(CoordinateTransform):
|
|
|
91
129
|
self.affine = affine
|
|
92
130
|
|
|
93
131
|
# array dimensions in reverse order (y = rows, x = cols)
|
|
94
|
-
self.xy_dims = self.dims[
|
|
95
|
-
self.dims = self.dims[
|
|
132
|
+
self.xy_dims = self.dims[XAXIS], self.dims[YAXIS]
|
|
133
|
+
self.dims = self.dims[YAXIS], self.dims[XAXIS]
|
|
96
134
|
|
|
97
135
|
def forward(self, dim_positions):
|
|
98
136
|
positions = tuple(dim_positions[dim] for dim in self.xy_dims)
|
|
@@ -114,13 +152,17 @@ class AffineTransform(CoordinateTransform):
|
|
|
114
152
|
|
|
115
153
|
return results
|
|
116
154
|
|
|
117
|
-
def equals(self, other, *, exclude):
|
|
155
|
+
def equals(self, other: CoordinateTransform, *, exclude: frozenset[Hashable] | None = None) -> bool:
|
|
118
156
|
if exclude is not None:
|
|
119
157
|
raise NotImplementedError
|
|
120
158
|
if not isinstance(other, AffineTransform):
|
|
121
159
|
return False
|
|
122
160
|
return self.affine == other.affine and self.dim_size == other.dim_size
|
|
123
161
|
|
|
162
|
+
def __repr__(self) -> str:
|
|
163
|
+
params = ", ".join(f"{pn}={getattr(self.affine, pn):.4g}" for pn in "abcdef")
|
|
164
|
+
return f"{type(self).__name__}({params})"
|
|
165
|
+
|
|
124
166
|
|
|
125
167
|
class AxisAffineTransform(CoordinateTransform):
|
|
126
168
|
"""Axis-independent wrapper of an affine 2D transform with no skew/rotation."""
|
|
@@ -169,7 +211,7 @@ class AxisAffineTransform(CoordinateTransform):
|
|
|
169
211
|
|
|
170
212
|
return {self.dim: positions}
|
|
171
213
|
|
|
172
|
-
def equals(self, other, *, exclude):
|
|
214
|
+
def equals(self, other: CoordinateTransform, *, exclude: frozenset[Hashable] | None = None) -> bool:
|
|
173
215
|
if not isinstance(other, AxisAffineTransform):
|
|
174
216
|
return False
|
|
175
217
|
if exclude is not None:
|
|
@@ -188,14 +230,10 @@ class AxisAffineTransform(CoordinateTransform):
|
|
|
188
230
|
return self.forward({self.dim: np.arange(self.size)})
|
|
189
231
|
|
|
190
232
|
def slice(self, slice: slice) -> AxisAffineTransform:
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
step =
|
|
194
|
-
|
|
195
|
-
# TODO: support reverse transform (i.e., start > stop)?
|
|
196
|
-
assert start < stop
|
|
197
|
-
|
|
198
|
-
size = (stop - start) // step
|
|
233
|
+
newrange = range(self.size)[slice]
|
|
234
|
+
start = newrange.start
|
|
235
|
+
step = newrange.step or 1
|
|
236
|
+
size = len(newrange)
|
|
199
237
|
scale = float(step)
|
|
200
238
|
|
|
201
239
|
if self.is_xaxis:
|
|
@@ -212,6 +250,10 @@ class AxisAffineTransform(CoordinateTransform):
|
|
|
212
250
|
dtype=self.dtype,
|
|
213
251
|
)
|
|
214
252
|
|
|
253
|
+
def __repr__(self) -> str:
|
|
254
|
+
params = ", ".join(f"{pn}={getattr(self.affine, pn):.4g}" for pn in "abcdef")
|
|
255
|
+
return f"{type(self).__name__}({params}, axis={'X' if self.is_xaxis else 'Y'}, dim={self.dim!r})"
|
|
256
|
+
|
|
215
257
|
|
|
216
258
|
class AxisAffineTransformIndex(CoordinateTransformIndex):
|
|
217
259
|
"""Axis-independent Xarray Index for an affine 2D transform with no
|
|
@@ -225,9 +267,6 @@ class AxisAffineTransformIndex(CoordinateTransformIndex):
|
|
|
225
267
|
- Data slicing computes a new affine transform and returns a new
|
|
226
268
|
`AxisAffineTransformIndex` object
|
|
227
269
|
|
|
228
|
-
- Otherwise data selection creates and returns a new Xarray
|
|
229
|
-
`PandasIndex` object for non-scalar indexers
|
|
230
|
-
|
|
231
270
|
- The index can be converted to a `pandas.Index` object (useful for Xarray
|
|
232
271
|
operations that don't work with Xarray indexes yet).
|
|
233
272
|
|
|
@@ -244,7 +283,7 @@ class AxisAffineTransformIndex(CoordinateTransformIndex):
|
|
|
244
283
|
|
|
245
284
|
def isel( # type: ignore[override]
|
|
246
285
|
self, indexers: Mapping[Any, int | slice | np.ndarray | Variable]
|
|
247
|
-
) -> AxisAffineTransformIndex |
|
|
286
|
+
) -> AxisAffineTransformIndex | None:
|
|
248
287
|
idxer = indexers[self.dim]
|
|
249
288
|
|
|
250
289
|
# generate a new index with updated transform if a slice is given
|
|
@@ -256,14 +295,17 @@ class AxisAffineTransformIndex(CoordinateTransformIndex):
|
|
|
256
295
|
# no index for scalar value
|
|
257
296
|
elif np.ndim(idxer) == 0:
|
|
258
297
|
return None
|
|
259
|
-
# otherwise
|
|
298
|
+
# otherwise drop the index
|
|
299
|
+
# (TODO: return a PandasIndex with values computed by forward transformation when it
|
|
300
|
+
# will be possible to auto-convert RasterIndex to PandasIndex for x and/or y axis)
|
|
260
301
|
else:
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
302
|
+
return None
|
|
303
|
+
# values = self.axis_transform.forward({self.dim: idxer})[self.axis_transform.coord_name]
|
|
304
|
+
# if isinstance(idxer, Variable):
|
|
305
|
+
# new_dim = idxer.dims[0]
|
|
306
|
+
# else:
|
|
307
|
+
# new_dim = self.dim
|
|
308
|
+
# return PandasIndex(values, new_dim, coord_dtype=values.dtype)
|
|
267
309
|
|
|
268
310
|
def sel(self, labels, method=None, tolerance=None):
|
|
269
311
|
coord_name = self.axis_transform.coord_name
|
|
@@ -311,22 +353,10 @@ class AxisAffineTransformIndex(CoordinateTransformIndex):
|
|
|
311
353
|
return pd.Index(values[self.dim])
|
|
312
354
|
|
|
313
355
|
|
|
314
|
-
|
|
315
|
-
WrappedIndex = AxisAffineTransformIndex | PandasIndex | CoordinateTransformIndex
|
|
316
|
-
WrappedIndexCoords = Hashable | tuple[Hashable, Hashable]
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
def _filter_dim_indexers(index: WrappedIndex, indexers: Mapping) -> Mapping:
|
|
320
|
-
if isinstance(index, CoordinateTransformIndex):
|
|
321
|
-
dims = index.transform.dims
|
|
322
|
-
else:
|
|
323
|
-
# PandasIndex
|
|
324
|
-
dims = (str(index.dim),)
|
|
356
|
+
WrappedIndex = tuple[AxisAffineTransformIndex, AxisAffineTransformIndex] | CoordinateTransformIndex
|
|
325
357
|
|
|
326
|
-
return {dim: indexers[dim] for dim in dims if dim in indexers}
|
|
327
358
|
|
|
328
|
-
|
|
329
|
-
class RasterIndex(Index):
|
|
359
|
+
class RasterIndex(Index, xproj.ProjIndexMixin):
|
|
330
360
|
"""Xarray index for raster coordinate indexing and spatial operations.
|
|
331
361
|
|
|
332
362
|
RasterIndex provides spatial indexing capabilities for raster data by wrapping
|
|
@@ -341,11 +371,13 @@ class RasterIndex(Index):
|
|
|
341
371
|
- **Rectilinear grids**: Uses separate 1D indexes for independent x/y axes,
|
|
342
372
|
enabling more efficient slicing operations.
|
|
343
373
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
374
|
+
RasterIndex is CRS-aware, i.e., it has a ``crs`` property that is used for
|
|
375
|
+
checking equality or compatibility with other RasterIndex instances. CRS is
|
|
376
|
+
optional.
|
|
377
|
+
|
|
378
|
+
Do not use :py:meth:`~rasterix.RasterIndex.__init__` directly. Instead use
|
|
379
|
+
:py:meth:`~rasterix.RasterIndex.from_transform` or
|
|
380
|
+
:py:func:`~rasterix.assign_index`.
|
|
349
381
|
|
|
350
382
|
Attributes
|
|
351
383
|
----------
|
|
@@ -376,38 +408,95 @@ class RasterIndex(Index):
|
|
|
376
408
|
For rectilinear grids without rotation, RasterIndex creates separate 1D indexes
|
|
377
409
|
for x and y coordinates, which enables efficient slicing operations. For grids
|
|
378
410
|
with rotation or skew, it uses a coupled 2D transform.
|
|
411
|
+
|
|
379
412
|
"""
|
|
380
413
|
|
|
381
|
-
|
|
382
|
-
|
|
414
|
+
_index: WrappedIndex
|
|
415
|
+
_axis_independent: bool
|
|
416
|
+
_crs: CRS | None
|
|
417
|
+
_xy_shape: tuple[int, int]
|
|
418
|
+
_xy_dims: tuple[str, str]
|
|
419
|
+
_xy_coord_names: tuple[Hashable, Hashable]
|
|
420
|
+
|
|
421
|
+
def __init__(self, index: WrappedIndex, crs: CRS | Any | None = None):
|
|
422
|
+
if isinstance(index, CoordinateTransformIndex) and isinstance(index.transform, AffineTransform):
|
|
423
|
+
self._axis_independent = False
|
|
424
|
+
xtransform = cast(AffineTransform, index.transform)
|
|
425
|
+
dim_size = xtransform.dim_size
|
|
426
|
+
xy_dims = xtransform.xy_dims
|
|
427
|
+
self._xy_shape = (dim_size[xy_dims[XAXIS]], dim_size[xy_dims[YAXIS]])
|
|
428
|
+
self._xy_dims = xtransform.xy_dims
|
|
429
|
+
self._xy_coord_names = (xtransform.coord_names[XAXIS], xtransform.coord_names[YAXIS])
|
|
430
|
+
elif (
|
|
431
|
+
isinstance(index, tuple)
|
|
432
|
+
and len(index) == 2
|
|
433
|
+
and isinstance(index[XAXIS], AxisAffineTransformIndex)
|
|
434
|
+
and isinstance(index[YAXIS], AxisAffineTransformIndex)
|
|
435
|
+
):
|
|
436
|
+
self._axis_independent = True
|
|
437
|
+
self._xy_shape = (index[XAXIS].axis_transform.size, index[YAXIS].axis_transform.size)
|
|
438
|
+
self._xy_dims = (index[XAXIS].axis_transform.dim, index[YAXIS].axis_transform.dim)
|
|
439
|
+
self._xy_coord_names = (
|
|
440
|
+
index[XAXIS].axis_transform.coord_name,
|
|
441
|
+
index[YAXIS].axis_transform.coord_name,
|
|
442
|
+
)
|
|
443
|
+
else:
|
|
444
|
+
raise ValueError(f"Could not create RasterIndex. Received invalid index {index!r}")
|
|
383
445
|
|
|
384
|
-
|
|
385
|
-
idx_keys = list(indexes)
|
|
386
|
-
idx_vals = list(indexes.values())
|
|
446
|
+
self._index = index
|
|
387
447
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
assert axis_dependent ^ axis_independent
|
|
398
|
-
|
|
399
|
-
self._wrapped_indexes = dict(indexes)
|
|
400
|
-
if axis_independent:
|
|
401
|
-
self._shape = {
|
|
402
|
-
"x": self._wrapped_indexes["x"].axis_transform.size,
|
|
403
|
-
"y": self._wrapped_indexes["y"].axis_transform.size,
|
|
404
|
-
}
|
|
448
|
+
if crs is not None:
|
|
449
|
+
crs = CRS.from_user_input(crs)
|
|
450
|
+
self._crs = crs
|
|
451
|
+
|
|
452
|
+
@property
|
|
453
|
+
def _wrapped_indexes(self) -> tuple[CoordinateTransformIndex | AxisAffineTransformIndex, ...]:
|
|
454
|
+
"""Returns the wrapped index objects as a tuple."""
|
|
455
|
+
if not isinstance(self._index, tuple):
|
|
456
|
+
return (self._index,)
|
|
405
457
|
else:
|
|
406
|
-
|
|
458
|
+
return self._index
|
|
459
|
+
|
|
460
|
+
@property
|
|
461
|
+
def _xy_indexes(self) -> tuple[AxisAffineTransformIndex, AxisAffineTransformIndex]:
|
|
462
|
+
assert self._axis_independent
|
|
463
|
+
return cast(tuple[AxisAffineTransformIndex, AxisAffineTransformIndex], self._index)
|
|
464
|
+
|
|
465
|
+
@property
|
|
466
|
+
def _xxyy_index(self) -> CoordinateTransformIndex:
|
|
467
|
+
assert not self._axis_independent
|
|
468
|
+
return cast(CoordinateTransformIndex, self._index)
|
|
469
|
+
|
|
470
|
+
@property
|
|
471
|
+
def xy_shape(self) -> tuple[int, int]:
|
|
472
|
+
"""Return the dimension size of the X and Y axis, respectively."""
|
|
473
|
+
return self._xy_shape
|
|
474
|
+
|
|
475
|
+
@property
|
|
476
|
+
def xy_dims(self) -> tuple[str, str]:
|
|
477
|
+
"""Return the dimension name of the X and Y axis, respectively."""
|
|
478
|
+
return self._xy_dims
|
|
479
|
+
|
|
480
|
+
@property
|
|
481
|
+
def xy_coord_names(self) -> tuple[Hashable, Hashable]:
|
|
482
|
+
"""Return the name of the coordinate variables representing labels on
|
|
483
|
+
the X and Y axis, respectively.
|
|
484
|
+
|
|
485
|
+
"""
|
|
486
|
+
return self._xy_coord_names
|
|
407
487
|
|
|
408
488
|
@classmethod
|
|
409
489
|
def from_transform(
|
|
410
|
-
cls,
|
|
490
|
+
cls,
|
|
491
|
+
affine: Affine,
|
|
492
|
+
*,
|
|
493
|
+
width: int,
|
|
494
|
+
height: int,
|
|
495
|
+
x_dim: str = "x",
|
|
496
|
+
y_dim: str = "y",
|
|
497
|
+
x_coord_name: str = "xc",
|
|
498
|
+
y_coord_name: str = "yc",
|
|
499
|
+
crs: CRS | Any | None = None,
|
|
411
500
|
) -> RasterIndex:
|
|
412
501
|
"""Create a RasterIndex from an affine transform and raster dimensions.
|
|
413
502
|
|
|
@@ -420,10 +509,17 @@ class RasterIndex(Index):
|
|
|
420
509
|
Number of pixels in the x direction.
|
|
421
510
|
height : int
|
|
422
511
|
Number of pixels in the y direction.
|
|
423
|
-
x_dim : str,
|
|
512
|
+
x_dim : str, optional
|
|
424
513
|
Name for the x dimension.
|
|
425
|
-
y_dim : str,
|
|
514
|
+
y_dim : str, optional
|
|
426
515
|
Name for the y dimension.
|
|
516
|
+
x_coord_name : str, optional
|
|
517
|
+
Name for the x dimension. For non-rectilinear transforms only.
|
|
518
|
+
y_coord_name : str, optional
|
|
519
|
+
Name for the y dimension. For non-rectilinear transforms only.
|
|
520
|
+
crs : :class:`pyproj.crs.CRS` or any, optional
|
|
521
|
+
The coordinate reference system. Any value accepted by
|
|
522
|
+
:meth:`pyproj.crs.CRS.from_user_input`.
|
|
427
523
|
|
|
428
524
|
Returns
|
|
429
525
|
-------
|
|
@@ -443,24 +539,38 @@ class RasterIndex(Index):
|
|
|
443
539
|
>>> from affine import Affine
|
|
444
540
|
>>> transform = Affine(1.0, 0.0, 0.0, 0.0, -1.0, 100.0)
|
|
445
541
|
>>> index = RasterIndex.from_transform(transform, width=100, height=100)
|
|
542
|
+
|
|
543
|
+
Create a non-rectilinear index:
|
|
544
|
+
|
|
545
|
+
>>> index = RasterIndex.from_transform(
|
|
546
|
+
... Affine.rotation(45), width=100, height=100, x_coord_name="x1", y_coord_name="x2"
|
|
547
|
+
... )
|
|
446
548
|
"""
|
|
447
|
-
|
|
549
|
+
index: WrappedIndex
|
|
448
550
|
|
|
449
551
|
# pixel centered coordinates
|
|
450
552
|
affine = affine * Affine.translation(0.5, 0.5)
|
|
451
553
|
|
|
452
554
|
if affine.is_rectilinear and affine.b == affine.d == 0:
|
|
453
|
-
x_transform = AxisAffineTransform(affine, width,
|
|
454
|
-
y_transform = AxisAffineTransform(affine, height,
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
555
|
+
x_transform = AxisAffineTransform(affine, width, x_dim, x_dim, is_xaxis=True)
|
|
556
|
+
y_transform = AxisAffineTransform(affine, height, y_dim, y_dim, is_xaxis=False)
|
|
557
|
+
index = (
|
|
558
|
+
AxisAffineTransformIndex(x_transform),
|
|
559
|
+
AxisAffineTransformIndex(y_transform),
|
|
560
|
+
)
|
|
459
561
|
else:
|
|
460
|
-
xy_transform = AffineTransform(
|
|
461
|
-
|
|
562
|
+
xy_transform = AffineTransform(
|
|
563
|
+
affine,
|
|
564
|
+
width,
|
|
565
|
+
height,
|
|
566
|
+
x_dim=x_dim,
|
|
567
|
+
y_dim=y_dim,
|
|
568
|
+
x_coord_name=x_coord_name,
|
|
569
|
+
y_coord_name=y_coord_name,
|
|
570
|
+
)
|
|
571
|
+
index = CoordinateTransformIndex(xy_transform)
|
|
462
572
|
|
|
463
|
-
return cls(
|
|
573
|
+
return cls(index, crs=crs)
|
|
464
574
|
|
|
465
575
|
@classmethod
|
|
466
576
|
def from_variables(
|
|
@@ -475,92 +585,116 @@ class RasterIndex(Index):
|
|
|
475
585
|
def create_variables(self, variables: Mapping[Any, Variable] | None = None) -> dict[Hashable, Variable]:
|
|
476
586
|
new_variables: dict[Hashable, Variable] = {}
|
|
477
587
|
|
|
478
|
-
for index in self._wrapped_indexes
|
|
588
|
+
for index in self._wrapped_indexes:
|
|
479
589
|
new_variables.update(index.create_variables())
|
|
480
590
|
|
|
591
|
+
if self.crs is not None:
|
|
592
|
+
xname, yname = self.xy_coord_names
|
|
593
|
+
xattrs, yattrs = self.crs.cs_to_cf()
|
|
594
|
+
if "axis" in xattrs and "axis" in yattrs:
|
|
595
|
+
# The axis order is defined by the projection
|
|
596
|
+
# So we have to figure which is which.
|
|
597
|
+
# This is an ugly hack that works for common cases.
|
|
598
|
+
if xattrs["axis"] == "Y":
|
|
599
|
+
xattrs, yattrs = yattrs, xattrs
|
|
600
|
+
new_variables[xname].attrs = xattrs
|
|
601
|
+
new_variables[yname].attrs = yattrs
|
|
602
|
+
|
|
481
603
|
return new_variables
|
|
482
604
|
|
|
605
|
+
@property
|
|
606
|
+
def crs(self) -> CRS | None:
|
|
607
|
+
"""Returns the coordinate reference system (CRS) of the index as a
|
|
608
|
+
:class:`pyproj.crs.CRS` object, or ``None`` if CRS is undefined.
|
|
609
|
+
"""
|
|
610
|
+
return self._crs
|
|
611
|
+
|
|
612
|
+
def _proj_set_crs(self: RasterIndex, spatial_ref: Hashable, crs: CRS) -> RasterIndex:
|
|
613
|
+
# Returns a raster index shallow copy with a replaced CRS
|
|
614
|
+
# (XProj integration via xproj.ProjIndexMixin)
|
|
615
|
+
# Note: XProj already handles the case of overriding any existing CRS
|
|
616
|
+
return RasterIndex(self._wrapped_indexes, crs=crs)
|
|
617
|
+
|
|
483
618
|
def isel(self, indexers: Mapping[Any, int | slice | np.ndarray | Variable]) -> RasterIndex | None:
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
for coord_names, index in self._wrapped_indexes.items():
|
|
487
|
-
index_indexers = _filter_dim_indexers(index, indexers)
|
|
488
|
-
if not index_indexers:
|
|
489
|
-
# no selection to perform: simply propagate the index
|
|
490
|
-
# TODO: uncomment when https://github.com/pydata/xarray/issues/10063 is fixed
|
|
491
|
-
# new_indexes[coord_names] = index
|
|
492
|
-
...
|
|
493
|
-
else:
|
|
494
|
-
new_index = index.isel(index_indexers)
|
|
495
|
-
if new_index is not None:
|
|
496
|
-
new_indexes[coord_names] = new_index
|
|
497
|
-
|
|
498
|
-
if new_indexes:
|
|
499
|
-
# TODO: if there's only a single PandasIndex can we just return it?
|
|
500
|
-
# (maybe better to keep it wrapped if we plan to later make RasterIndex CRS-aware)
|
|
501
|
-
return RasterIndex(new_indexes)
|
|
502
|
-
else:
|
|
619
|
+
if not self._axis_independent:
|
|
620
|
+
# preserve RasterIndex is not supported in the case of coupled x/y 2D coordinates
|
|
503
621
|
return None
|
|
622
|
+
else:
|
|
623
|
+
new_indexes = []
|
|
624
|
+
|
|
625
|
+
for index in self._xy_indexes:
|
|
626
|
+
dim = index.axis_transform.dim
|
|
627
|
+
|
|
628
|
+
if dim not in indexers:
|
|
629
|
+
# simply propagate the index
|
|
630
|
+
new_indexes.append(index)
|
|
631
|
+
else:
|
|
632
|
+
new_index = index.isel({dim: indexers[dim]})
|
|
633
|
+
if new_index is not None:
|
|
634
|
+
new_indexes.append(new_index)
|
|
635
|
+
|
|
636
|
+
# TODO: if/when supported in Xarray, return PandasIndex instances for either the
|
|
637
|
+
# x or the y axis (or both) instead of returning None (drop the index)
|
|
638
|
+
if len(new_indexes) == 2:
|
|
639
|
+
return RasterIndex(tuple(new_indexes), crs=self.crs)
|
|
640
|
+
else:
|
|
641
|
+
return None
|
|
504
642
|
|
|
505
643
|
def sel(self, labels: dict[Any, Any], method=None, tolerance=None) -> IndexSelResult:
|
|
506
|
-
|
|
644
|
+
if not self._axis_independent:
|
|
645
|
+
return self._xxyy_index.sel(labels, method=method, tolerance=tolerance)
|
|
646
|
+
else:
|
|
647
|
+
results: list[IndexSelResult] = []
|
|
507
648
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
results.append(index.sel(index_labels, method=method, tolerance=tolerance))
|
|
649
|
+
for index in self._xy_indexes:
|
|
650
|
+
coord_name = index.axis_transform.coord_name
|
|
651
|
+
if coord_name in labels:
|
|
652
|
+
res = index.sel({coord_name: labels[coord_name]}, method=method, tolerance=tolerance)
|
|
653
|
+
results.append(res)
|
|
514
654
|
|
|
515
|
-
|
|
655
|
+
return merge_sel_results(results)
|
|
516
656
|
|
|
517
657
|
def equals(self, other: Index, *, exclude=None) -> bool:
|
|
518
658
|
if exclude is None:
|
|
519
659
|
exclude = {}
|
|
660
|
+
|
|
520
661
|
if not isinstance(other, RasterIndex):
|
|
521
662
|
return False
|
|
522
|
-
if
|
|
663
|
+
if not self._proj_crs_equals(cast(CRSAwareIndex, other), allow_none=True):
|
|
664
|
+
return False
|
|
665
|
+
if self._axis_independent != other._axis_independent:
|
|
523
666
|
return False
|
|
524
667
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
# exactly one AxisAffineTransformIndex or a PandasIndex associated
|
|
534
|
-
# to either the x or y axis (1-dimensional) coordinate.
|
|
535
|
-
if len(self._wrapped_indexes) == 1:
|
|
536
|
-
index = next(iter(self._wrapped_indexes.values()))
|
|
537
|
-
if isinstance(index, AxisAffineTransformIndex | PandasIndex):
|
|
538
|
-
return index.to_pandas_index()
|
|
539
|
-
|
|
540
|
-
raise ValueError("Cannot convert RasterIndex to pandas.Index")
|
|
541
|
-
|
|
542
|
-
def __repr__(self):
|
|
543
|
-
items: list[str] = []
|
|
544
|
-
|
|
545
|
-
for coord_names, index in self._wrapped_indexes.items():
|
|
546
|
-
items += [repr(coord_names) + ":", textwrap.indent(repr(index), " ")]
|
|
547
|
-
|
|
548
|
-
return "RasterIndex\n" + "\n".join(items)
|
|
668
|
+
if self._axis_independent:
|
|
669
|
+
return all(
|
|
670
|
+
idx.equals(other_idx)
|
|
671
|
+
for idx, other_idx in zip(self._xy_indexes, other._xy_indexes)
|
|
672
|
+
if idx.axis_transform.dim not in exclude
|
|
673
|
+
)
|
|
674
|
+
else:
|
|
675
|
+
return self._xxyy_index.equals(other._xxyy_index)
|
|
549
676
|
|
|
550
677
|
def transform(self) -> Affine:
|
|
551
678
|
"""Affine transform for top-left corners."""
|
|
552
679
|
return self.center_transform() * Affine.translation(-0.5, -0.5)
|
|
553
680
|
|
|
681
|
+
def as_geotransform(self, *, decimals: int | None = None) -> str:
|
|
682
|
+
"""Convert the affine transform to a string suitable for saving as the GeoTransform attribute."""
|
|
683
|
+
gt = self.transform().to_gdal()
|
|
684
|
+
if decimals is not None:
|
|
685
|
+
fmt = f".{decimals}f"
|
|
686
|
+
return " ".join(f"{num:{fmt}}" for num in gt)
|
|
687
|
+
else:
|
|
688
|
+
return " ".join(map(str, gt))
|
|
689
|
+
|
|
554
690
|
def center_transform(self) -> Affine:
|
|
555
691
|
"""Affine transform for cell centers."""
|
|
556
|
-
if
|
|
557
|
-
|
|
558
|
-
y = self._wrapped_indexes["y"].axis_transform.affine
|
|
559
|
-
aff = Affine(x.a, x.b, x.c, y.d, y.e, y.f)
|
|
692
|
+
if not self._axis_independent:
|
|
693
|
+
return cast(AffineTransform, self._xxyy_index.transform).affine
|
|
560
694
|
else:
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
695
|
+
x = self._xy_indexes[XAXIS].axis_transform.affine
|
|
696
|
+
y = self._xy_indexes[YAXIS].axis_transform.affine
|
|
697
|
+
return Affine(x.a, x.b, x.c, y.d, y.e, y.f)
|
|
564
698
|
|
|
565
699
|
@property
|
|
566
700
|
def bbox(self) -> BoundingBox:
|
|
@@ -570,10 +704,8 @@ class RasterIndex(Index):
|
|
|
570
704
|
-------
|
|
571
705
|
BoundingBox
|
|
572
706
|
"""
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
transform=self.transform(),
|
|
576
|
-
)
|
|
707
|
+
yx_shape = (self.xy_shape[YAXIS], self.xy_shape[XAXIS])
|
|
708
|
+
return BoundingBox.from_transform(shape=yx_shape, transform=self.transform())
|
|
577
709
|
|
|
578
710
|
@classmethod
|
|
579
711
|
def concat(
|
|
@@ -581,7 +713,7 @@ class RasterIndex(Index):
|
|
|
581
713
|
indexes: Sequence[Self],
|
|
582
714
|
dim: Hashable,
|
|
583
715
|
positions: Iterable[Iterable[int]] | None = None,
|
|
584
|
-
) ->
|
|
716
|
+
) -> RasterIndex:
|
|
585
717
|
if len(indexes) == 1:
|
|
586
718
|
return next(iter(indexes))
|
|
587
719
|
|
|
@@ -593,7 +725,7 @@ class RasterIndex(Index):
|
|
|
593
725
|
new_bbox = bbox_union(as_compatible_bboxes(*indexes, concat_dim=dim))
|
|
594
726
|
return indexes[0]._new_with_bbox(new_bbox)
|
|
595
727
|
|
|
596
|
-
def _new_with_bbox(self, bbox: BoundingBox) ->
|
|
728
|
+
def _new_with_bbox(self, bbox: BoundingBox) -> RasterIndex:
|
|
597
729
|
affine = self.transform()
|
|
598
730
|
new_affine, Nx, Ny = bbox_to_affine(bbox, rx=affine.a, ry=affine.e)
|
|
599
731
|
# TODO: set xdim, ydim explicitly
|
|
@@ -601,8 +733,14 @@ class RasterIndex(Index):
|
|
|
601
733
|
assert new_index.bbox == bbox
|
|
602
734
|
return new_index
|
|
603
735
|
|
|
604
|
-
def join(self, other: RasterIndex, how) -> RasterIndex:
|
|
605
|
-
if
|
|
736
|
+
def join(self, other: RasterIndex, how: JoinOptions = "inner") -> RasterIndex:
|
|
737
|
+
if not self._proj_crs_equals(cast(CRSAwareIndex, other), allow_none=True):
|
|
738
|
+
raise ValueError(
|
|
739
|
+
"raster indexes on objects to align do not have the same CRS\n"
|
|
740
|
+
f"first index:\n{self!r}\n\nsecond index:\n{other!r}"
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
if len(self._wrapped_indexes) != len(other._wrapped_indexes):
|
|
606
744
|
# TODO: better error message
|
|
607
745
|
raise ValueError(
|
|
608
746
|
"Alignment is only supported between RasterIndexes, when both contain compatible transforms."
|
|
@@ -619,6 +757,7 @@ class RasterIndex(Index):
|
|
|
619
757
|
return self._new_with_bbox(new_bbox)
|
|
620
758
|
|
|
621
759
|
def reindex_like(self, other: Self, method=None, tolerance=None) -> dict[Hashable, Any]:
|
|
760
|
+
x_dim, y_dim = self.xy_dims
|
|
622
761
|
affine = self.transform()
|
|
623
762
|
ours, theirs = as_compatible_bboxes(self, other, concat_dim=None)
|
|
624
763
|
inter = bbox_intersection([ours, theirs])
|
|
@@ -631,14 +770,35 @@ class RasterIndex(Index):
|
|
|
631
770
|
tol: float = 0.01
|
|
632
771
|
|
|
633
772
|
indexers = {}
|
|
634
|
-
indexers[
|
|
635
|
-
theirs.left, ours.left, inter.left, inter.right, spacing=dx, tol=tol, size=other.
|
|
773
|
+
indexers[x_dim] = get_indexer(
|
|
774
|
+
theirs.left, ours.left, inter.left, inter.right, spacing=dx, tol=tol, size=other.xy_shape[XAXIS]
|
|
636
775
|
)
|
|
637
|
-
indexers[
|
|
638
|
-
theirs.top, ours.top, inter.top, inter.bottom, spacing=dy, tol=tol, size=other.
|
|
776
|
+
indexers[y_dim] = get_indexer(
|
|
777
|
+
theirs.top, ours.top, inter.top, inter.bottom, spacing=dy, tol=tol, size=other.xy_shape[YAXIS]
|
|
639
778
|
)
|
|
640
779
|
return indexers
|
|
641
780
|
|
|
781
|
+
def _repr_inline_(self, max_width: int) -> str:
|
|
782
|
+
# TODO: remove when fixed in Xarray (https://github.com/pydata/xarray/pull/10415)
|
|
783
|
+
if max_width is None:
|
|
784
|
+
max_width = get_options()["display_width"]
|
|
785
|
+
|
|
786
|
+
srs = xproj.format_crs(self.crs, max_width=max_width)
|
|
787
|
+
return f"{self.__class__.__name__} (crs={srs})"
|
|
788
|
+
|
|
789
|
+
def __repr__(self) -> str:
|
|
790
|
+
srs = xproj.format_crs(self.crs)
|
|
791
|
+
|
|
792
|
+
def index_repr(idx) -> str:
|
|
793
|
+
return textwrap.indent(f"{type(idx).__name__}({idx.transform!r})", " ")
|
|
794
|
+
|
|
795
|
+
if self._axis_independent:
|
|
796
|
+
idx_repr = "\n".join(index_repr(idx) for idx in self._xy_indexes)
|
|
797
|
+
else:
|
|
798
|
+
idx_repr = index_repr(self._xxyy_index)
|
|
799
|
+
|
|
800
|
+
return f"RasterIndex(crs={srs})\n{idx_repr}"
|
|
801
|
+
|
|
642
802
|
|
|
643
803
|
def get_indexer(off, our_off, start, stop, spacing, tol, size) -> np.ndarray:
|
|
644
804
|
istart = math.ceil(maybe_int((start - off) / spacing, tol))
|
|
@@ -656,7 +816,7 @@ def get_indexer(off, our_off, start, stop, spacing, tol, size) -> np.ndarray:
|
|
|
656
816
|
return idxr
|
|
657
817
|
|
|
658
818
|
|
|
659
|
-
def bbox_to_affine(bbox: BoundingBox, rx, ry) -> Affine:
|
|
819
|
+
def bbox_to_affine(bbox: BoundingBox, rx, ry) -> tuple[Affine, int, int]:
|
|
660
820
|
# Fraction of a pixel that can be ignored, defaults to 1/100. Bounding box of the output
|
|
661
821
|
# geobox is allowed to be smaller than supplied bounding box by that amount.
|
|
662
822
|
# FIXME: translate user-provided `tolerance` to `tol`
|
|
@@ -675,10 +835,10 @@ def as_compatible_bboxes(*indexes: RasterIndex, concat_dim: Hashable | None) ->
|
|
|
675
835
|
_assert_transforms_are_compatible(*transforms)
|
|
676
836
|
|
|
677
837
|
expected_off_x = (transforms[0].c,) + tuple(
|
|
678
|
-
t.c + i.
|
|
838
|
+
t.c + i.xy_shape[XAXIS] * t.a for i, t in zip(indexes[:-1], transforms[:-1])
|
|
679
839
|
)
|
|
680
840
|
expected_off_y = (transforms[0].f,) + tuple(
|
|
681
|
-
t.f + i.
|
|
841
|
+
t.f + i.xy_shape[YAXIS] * t.e for i, t in zip(indexes[:-1], transforms[:-1])
|
|
682
842
|
)
|
|
683
843
|
|
|
684
844
|
off_x = tuple(t.c for t in transforms)
|
|
@@ -688,14 +848,17 @@ def as_compatible_bboxes(*indexes: RasterIndex, concat_dim: Hashable | None) ->
|
|
|
688
848
|
if all(o == off_x[0] for o in off_x[1:]) and all(o == off_y[0] for o in off_y[1:]):
|
|
689
849
|
raise ValueError("Attempting to concatenate arrays with same transform along X or Y.")
|
|
690
850
|
|
|
691
|
-
|
|
851
|
+
# note: Xarray alignment already ensures that the indexes dimensions are compatible.
|
|
852
|
+
x_dim, y_dim = indexes[0].xy_dims
|
|
853
|
+
|
|
854
|
+
if concat_dim == x_dim:
|
|
692
855
|
if any(off_y[0] != o for o in off_y[1:]):
|
|
693
856
|
raise ValueError("offsets must be identical in X when concatenating along Y")
|
|
694
857
|
if any(a != b for a, b in zip(off_x, expected_off_x)):
|
|
695
858
|
raise ValueError(
|
|
696
859
|
f"X offsets are incompatible. Provided offsets {off_x}, expected offsets: {expected_off_x}"
|
|
697
860
|
)
|
|
698
|
-
elif concat_dim ==
|
|
861
|
+
elif concat_dim == y_dim:
|
|
699
862
|
if any(off_x[0] != o for o in off_x[1:]):
|
|
700
863
|
raise ValueError("offsets must be identical in X when concatenating along Y")
|
|
701
864
|
|
rasterix/rasterize/rasterio.py
CHANGED
|
@@ -15,7 +15,8 @@ from rasterio.features import MergeAlg
|
|
|
15
15
|
from rasterio.features import geometry_mask as geometry_mask_rio
|
|
16
16
|
from rasterio.features import rasterize as rasterize_rio
|
|
17
17
|
|
|
18
|
-
from
|
|
18
|
+
from ..utils import get_affine
|
|
19
|
+
from .utils import XAXIS, YAXIS, clip_to_bbox, is_in_memory, prepare_for_dask
|
|
19
20
|
|
|
20
21
|
F = TypeVar("F", bound=Callable[..., Any])
|
|
21
22
|
|
|
@@ -161,7 +162,7 @@ def rasterize(
|
|
|
161
162
|
obj = clip_to_bbox(obj, geometries, xdim=xdim, ydim=ydim)
|
|
162
163
|
|
|
163
164
|
rasterize_kwargs = dict(
|
|
164
|
-
all_touched=all_touched, merge_alg=merge_alg, affine=get_affine(obj,
|
|
165
|
+
all_touched=all_touched, merge_alg=merge_alg, affine=get_affine(obj, x_dim=xdim, y_dim=ydim), env=env
|
|
165
166
|
)
|
|
166
167
|
# FIXME: box.crs == geometries.crs
|
|
167
168
|
|
|
@@ -325,7 +326,7 @@ def geometry_mask(
|
|
|
325
326
|
obj = clip_to_bbox(obj, geometries, xdim=xdim, ydim=ydim)
|
|
326
327
|
|
|
327
328
|
geometry_mask_kwargs = dict(
|
|
328
|
-
all_touched=all_touched, affine=get_affine(obj,
|
|
329
|
+
all_touched=all_touched, affine=get_affine(obj, x_dim=xdim, y_dim=ydim), env=env
|
|
329
330
|
)
|
|
330
331
|
|
|
331
332
|
if is_in_memory(obj=obj, geometries=geometries):
|
rasterix/utils.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import xarray as xr
|
|
2
|
+
from affine import Affine
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def get_grid_mapping_var(obj: xr.Dataset | xr.DataArray) -> xr.DataArray | None:
|
|
6
|
+
grid_mapping_var = None
|
|
7
|
+
if isinstance(obj, xr.DataArray):
|
|
8
|
+
if maybe := obj.attrs.get("grid_mapping", None):
|
|
9
|
+
if maybe in obj.coords:
|
|
10
|
+
grid_mapping_var = maybe
|
|
11
|
+
else:
|
|
12
|
+
# for datasets, grab the first one for simplicity
|
|
13
|
+
for var in obj.data_vars.values():
|
|
14
|
+
if maybe := var.attrs.get("grid_mapping"):
|
|
15
|
+
if maybe in obj.coords:
|
|
16
|
+
# make sure it exists and is not an out-of-date attribute
|
|
17
|
+
grid_mapping_var = maybe
|
|
18
|
+
break
|
|
19
|
+
if grid_mapping_var is None and "spatial_ref" in obj.coords:
|
|
20
|
+
# hardcode this
|
|
21
|
+
grid_mapping_var = "spatial_ref"
|
|
22
|
+
if grid_mapping_var is not None:
|
|
23
|
+
return obj[grid_mapping_var]
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_affine(
|
|
28
|
+
obj: xr.Dataset | xr.DataArray, *, x_dim="x", y_dim="y", clear_transform: bool = False
|
|
29
|
+
) -> Affine:
|
|
30
|
+
"""
|
|
31
|
+
Grabs an affine transform from an Xarray object.
|
|
32
|
+
|
|
33
|
+
This method will first look for the ``"GeoTransform"`` attribute on a variable named
|
|
34
|
+
``"spatial_ref"``. If not, it will auto-guess the transform from the provided ``x_dim``,
|
|
35
|
+
and ``y_dim``.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
obj: xr.DataArray or xr.Dataset
|
|
40
|
+
x_dim: str, optional
|
|
41
|
+
Name of the X dimension coordinate variable.
|
|
42
|
+
y_dim: str, optional
|
|
43
|
+
Name of the Y dimension coordinate variable.
|
|
44
|
+
clear_transform: bool
|
|
45
|
+
Whether to delete the ``GeoTransform`` attribute if detected.
|
|
46
|
+
|
|
47
|
+
Returns
|
|
48
|
+
-------
|
|
49
|
+
affine: Affine
|
|
50
|
+
"""
|
|
51
|
+
grid_mapping_var = get_grid_mapping_var(obj)
|
|
52
|
+
if grid_mapping_var is not None and (transform := grid_mapping_var.attrs.get("GeoTransform")):
|
|
53
|
+
if clear_transform:
|
|
54
|
+
del grid_mapping_var.attrs["GeoTransform"]
|
|
55
|
+
return Affine.from_gdal(*map(float, transform.split(" ")))
|
|
56
|
+
else:
|
|
57
|
+
x = obj.coords[x_dim]
|
|
58
|
+
y = obj.coords[y_dim]
|
|
59
|
+
if x.ndim != 1:
|
|
60
|
+
raise ValueError(f"Coordinate variable {x_dim=!r} must be 1D.")
|
|
61
|
+
if y.ndim != 1:
|
|
62
|
+
raise ValueError(f"Coordinate variable {y_dim=!r} must be 1D.")
|
|
63
|
+
dx = (x[1] - x[0]).item()
|
|
64
|
+
dy = (y[1] - y[0]).item()
|
|
65
|
+
return Affine.translation(
|
|
66
|
+
x[0].item() - dx / 2, (y[0] if dy < 0 else y[-1]).item() - dy / 2
|
|
67
|
+
) * Affine.scale(dx, dy)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rasterix
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.1a4
|
|
4
4
|
Summary: Raster extensions for Xarray
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -17,6 +17,7 @@ Requires-Dist: affine
|
|
|
17
17
|
Requires-Dist: numpy>=2
|
|
18
18
|
Requires-Dist: pandas>=2
|
|
19
19
|
Requires-Dist: xarray>=2025
|
|
20
|
+
Requires-Dist: xproj>=0.2.0
|
|
20
21
|
Provides-Extra: dask
|
|
21
22
|
Requires-Dist: dask-geopandas; extra == 'dask'
|
|
22
23
|
Provides-Extra: docs
|
|
@@ -52,57 +53,30 @@ Requires-Dist: hypothesis; extra == 'test'
|
|
|
52
53
|
Requires-Dist: netcdf4; extra == 'test'
|
|
53
54
|
Requires-Dist: pooch; extra == 'test'
|
|
54
55
|
Requires-Dist: rasterio; extra == 'test'
|
|
55
|
-
Requires-Dist: rioxarray; extra == 'test'
|
|
56
56
|
Requires-Dist: sparse; extra == 'test'
|
|
57
57
|
Description-Content-Type: text/markdown
|
|
58
58
|
|
|
59
59
|
# rasterix: Raster tricks for Xarray
|
|
60
60
|
|
|
61
|
-
[](https://github.com/xarray-contrib/rasterix/actions)
|
|
62
62
|
[](https://rasterix.readthedocs.io/en/latest/?badge=latest)
|
|
63
63
|
[](https://pypi.org/project/rasterix/)
|
|
64
64
|
[](https://anaconda.org/conda-forge/rasterix)
|
|
65
65
|
|
|
66
|
-
<img src="rasterix.png" width="300">
|
|
66
|
+
<img src="_static/rasterix.png" width="300">
|
|
67
67
|
|
|
68
68
|
This WIP project contains tools to make it easier to analyze raster data with Xarray.
|
|
69
|
-
|
|
70
|
-
The intent is to provide reusable building blocks for the many sub-ecosystems around: e.g. rioxarray, odc-geo, etc.
|
|
71
|
-
|
|
72
|
-
## Contents
|
|
73
|
-
|
|
74
69
|
It currently has two pieces.
|
|
75
70
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
See `src/ rasterix/raster_index.py` and `notebooks/raster_index.ipynb` for a brief demo.
|
|
79
|
-
|
|
80
|
-
### 2. Dask-aware rasterization wrappers
|
|
71
|
+
1. `RasterIndex` for indexing using the affine transform recorded in GeoTIFFs.
|
|
72
|
+
1. Dask-aware rasterization wrappers around `exactextract`, `rasterio.features.rasterize`, and `rasterio.features.geometry_mask`.
|
|
81
73
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
This code is likely to move elsewhere!
|
|
74
|
+
Our intent is to provide reusable building blocks for the many sub-ecosystems around: e.g. `rioxarray`, `odc.geo`, etc.
|
|
85
75
|
|
|
86
76
|
## Installing
|
|
87
77
|
|
|
88
|
-
### PyPI
|
|
89
|
-
|
|
90
78
|
`rasterix` alpha releases are available on pypi
|
|
91
79
|
|
|
92
80
|
```
|
|
93
81
|
pip install rasterix
|
|
94
82
|
```
|
|
95
|
-
|
|
96
|
-
## Developing
|
|
97
|
-
|
|
98
|
-
1. Clone the repo
|
|
99
|
-
```
|
|
100
|
-
git remote add upstream git@github.com:dcherian/rasterix.git
|
|
101
|
-
cd rasterix
|
|
102
|
-
```
|
|
103
|
-
1. [Install hatch](https://hatch.pypa.io/1.12/install/)
|
|
104
|
-
1. Run the tests
|
|
105
|
-
```
|
|
106
|
-
hatch env run --env test.py3.13 run-pytest # Run the tests without coverage reports
|
|
107
|
-
hatch env run --env test.py3.13 run-coverage-html # Run the tests with an html coverage report
|
|
108
|
-
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
rasterix/__init__.py,sha256=iSC54A4b-7zgxtKV0CUok0O2-ApGtzVA-6hWG3a5AAA,283
|
|
2
|
+
rasterix/_version.py,sha256=Rlam12WbDAQ2TzSYxzdnw4SLFERCr6sX8OCMwKNxEX4,514
|
|
3
|
+
rasterix/odc_compat.py,sha256=MxjctCH0zV6VSSQymatJreHYUnUo3qkB1oIKbOxEK4A,19099
|
|
4
|
+
rasterix/raster_index.py,sha256=uwEDjjmnESWpyqsJwAnZvpWrjnKDeg0m1qNNH9mYTWY,32808
|
|
5
|
+
rasterix/rioxarray_compat.py,sha256=o32UBkWBpNhmC35ar0Ozn2QszpWdPvdDDXif3Mq2ZKg,12428
|
|
6
|
+
rasterix/utils.py,sha256=y2N-3d9-D8tBKjeZXv4vcjY_fs_dbbx5SfBBkQibLNs,2441
|
|
7
|
+
rasterix/rasterize/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
rasterix/rasterize/exact.py,sha256=UPB9zXpj7ZgkDy-x4vcNwY3J8DTrOTkRcIBMkhPQ04s,10558
|
|
9
|
+
rasterix/rasterize/rasterio.py,sha256=gtdQxM9CiosalYk3rLKGkp0UmhZVO5parAVsR7IINoE,12883
|
|
10
|
+
rasterix/rasterize/utils.py,sha256=iIuyTdigpFnts_-bUfas6FMsgix5lMqMQTxQaEBlyXM,3521
|
|
11
|
+
rasterix-0.1a4.dist-info/METADATA,sha256=K5l2PDTNLvmdA6TVGvy2tg5-YRevdmhV5mYX_Os3Rbw,3402
|
|
12
|
+
rasterix-0.1a4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
13
|
+
rasterix-0.1a4.dist-info/licenses/LICENSE,sha256=QFnsASMx8_yBNbrS7GVOhJ5CglGsLMj83Rn61uWyMs8,10265
|
|
14
|
+
rasterix-0.1a4.dist-info/RECORD,,
|
rasterix-0.1a3.dist-info/RECORD
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
rasterix/__init__.py,sha256=iSC54A4b-7zgxtKV0CUok0O2-ApGtzVA-6hWG3a5AAA,283
|
|
2
|
-
rasterix/_version.py,sha256=hJd92hgvVD0aLSzXDQZBYe_xFmMQly9wq_1h2CuGX_g,514
|
|
3
|
-
rasterix/odc_compat.py,sha256=MxjctCH0zV6VSSQymatJreHYUnUo3qkB1oIKbOxEK4A,19099
|
|
4
|
-
rasterix/raster_index.py,sha256=yCq08-BvgEWe5ZbtknkMsXqKkZVqGyw85Otcx3_atf4,26130
|
|
5
|
-
rasterix/rioxarray_compat.py,sha256=o32UBkWBpNhmC35ar0Ozn2QszpWdPvdDDXif3Mq2ZKg,12428
|
|
6
|
-
rasterix/rasterize/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
-
rasterix/rasterize/exact.py,sha256=UPB9zXpj7ZgkDy-x4vcNwY3J8DTrOTkRcIBMkhPQ04s,10558
|
|
8
|
-
rasterix/rasterize/rasterio.py,sha256=TdnDMGfsN3npP_LPEPXH2LV25AJEkmEuTOpke5GN8_8,12860
|
|
9
|
-
rasterix/rasterize/utils.py,sha256=iIuyTdigpFnts_-bUfas6FMsgix5lMqMQTxQaEBlyXM,3521
|
|
10
|
-
rasterix-0.1a3.dist-info/METADATA,sha256=05YXAANNx2f50SqN_bXO_N7gb1YR29S3GIMEmi1mEXQ,4262
|
|
11
|
-
rasterix-0.1a3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
12
|
-
rasterix-0.1a3.dist-info/licenses/LICENSE,sha256=QFnsASMx8_yBNbrS7GVOhJ5CglGsLMj83Rn61uWyMs8,10265
|
|
13
|
-
rasterix-0.1a3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|