Skip to content

Commit e144693

Browse files
dcherianclaude
andauthored
assign_index: Add GeoTIFF & STAC metadata support (#56)
Co-authored-by: Claude <[email protected]>
1 parent bbe7b0c commit e144693

File tree

10 files changed

+800
-4650
lines changed

10 files changed

+800
-4650
lines changed

docs/heuristics.md

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# `assign_index` Heuristics
2+
3+
This page documents the heuristics and decision-making logic used by rasterix when working with spatial data.
4+
5+
## Affine Transform Detection in `assign_index`
6+
7+
When calling {py:func}`assign_index()` to create a `RasterIndex` for an Xarray object, rasterix needs to determine the affine transformation that maps pixel coordinates to spatial coordinates. The function follows a specific priority order when searching for this information.
8+
9+
### Priority Order
10+
11+
Rasterix checks for transform information in the following order:
12+
13+
1. `GeoTransform` attribute on a CF 'grid mapping' variable (commonly `spatial_ref`).
14+
1. STAC `proj:transform` attribute
15+
1. GeoTIFF metadata: (`model_tiepoint` and `model_pixel_scale` for now)
16+
17+
Each method is tried in sequence, and the first successful match is used.
18+
19+
If these fail, an index is constructed from explicit coordinate arrays if present.
20+
21+
### 1. `GeoTransform`
22+
23+
```{seealso}
24+
[GDAL GeoTransform tutorial](https://gdal.org/en/stable/tutorials/geotransforms_tut.html)
25+
```
26+
27+
The function first looks for a CF conventions "grid mapping" variable (commonly named `spatial_ref`). The grid mapping variable is identified by:
28+
29+
- For DataArrays: the `grid_mapping` attribute on the DataArray itself
30+
- For Datasets: the `grid_mapping` attribute on the first data variable
31+
- As a fallback: a coordinate variable named `spatial_ref`
32+
33+
If found, rasterix checks for a `GeoTransform` attribute on this variable, which should be a GDAL-format geotransform string with 6 space-separated numbers.
34+
35+
**Format**: `"c a b f d e"` (GDAL format)
36+
37+
**Example**:
38+
39+
```python
40+
da.coords["spatial_ref"].attrs["GeoTransform"] = "323400.0 30.0 0.0 4265400.0 0.0 30.0"
41+
```
42+
43+
### 2. STAC `proj:transform`
44+
45+
```{seealso}
46+
[STAC Projection Extension](https://github.com/stac-extensions/projection)
47+
```
48+
49+
If no GeoTransform is found, rasterix checks the DataArray's attributes for a STAC projection extension `proj:transform` field. This represents the affine transformation as a flat array.
50+
51+
**Format**: `[a, b, c, d, e, f]` or `[a, b, c, d, e, f, 0, 0, 1]`
52+
53+
The transformation follows this matrix formula:
54+
55+
```
56+
[Xp] [a b c] [Pixel]
57+
[Yp] = [d e f] * [Line ]
58+
[1 ] [0 0 1] [1 ]
59+
```
60+
61+
Where:
62+
63+
- `a` = pixel width (x-scale)
64+
- `b` = row rotation (typically 0)
65+
- `c` = x-coordinate of upper-left pixel corner
66+
- `d` = column rotation (typically 0)
67+
- `e` = pixel height (y-scale, negative if y decreases)
68+
- `f` = y-coordinate of upper-left pixel corner
69+
70+
**Example**:
71+
72+
```python
73+
da.attrs["proj:transform"] = [30.0, 0.0, 323400.0, 0.0, 30.0, 4268400.0]
74+
```
75+
76+
### 3. GeoTIFF Metadata
77+
78+
```{seealso}
79+
[GeoTIFF specification on coordinate transformations](https://docs.ogc.org/is/19-008r4/19-008r4.html#_geotiff_tags_for_coordinate_transformations)
80+
```
81+
82+
If no STAC transform is found, rasterix checks for GeoTIFF-style metadata using model tiepoints and pixel scale.
83+
84+
**Format**:
85+
86+
- `model_tiepoint`: `[I, J, K, X, Y, Z]` - Maps pixel `(I, J, K)` to world coordinates `(X, Y, Z)`
87+
- `model_pixel_scale`: `[ScaleX, ScaleY, ScaleZ]` - Pixel dimensions in world coordinates
88+
89+
**Constraints**:
90+
91+
- `ScaleZ` must be 0 (only 2D rasters are supported)
92+
- If the tiepoint is at pixel `(I, J)`, the affine is computed as:
93+
- `c = X - I * ScaleX`
94+
- `f = Y - J * ScaleY`
95+
96+
**Example**:
97+
98+
```python
99+
da.attrs["model_tiepoint"] = [0.0, 0.0, 0.0, 323400.0, 4265400.0, 0.0]
100+
da.attrs["model_pixel_scale"] = [30.0, 30.0, 0.0]
101+
```
102+
103+
**Trace log**: `"Creating affine from GeoTIFF model_tiepoint and model_pixel_scale attributes"`
104+
105+
### 4. Coordinate Arrays
106+
107+
If no metadata is found, rasterix falls back to computing the affine transformation from 1D coordinate arrays.
108+
109+
**Requirements**:
110+
111+
- Coordinate variables must exist for both x and y dimensions
112+
- Coordinates must be 1D arrays
113+
- Coordinates must have at least 2 values to compute spacing
114+
115+
**Computation**:
116+
117+
- Pixel spacing: `dx = x[1] - x[0]`, `dy = y[1] - y[0]`
118+
- Origin calculation accounts for whether y increases or decreases
119+
- Assumes pixel-centered coordinates, adjusts to pixel corners
120+
121+
**Example**:
122+
123+
```python
124+
da = xr.DataArray(
125+
data,
126+
coords={
127+
"x": np.arange(0.5, 100.5), # Pixel-centered
128+
"y": np.arange(0.5, 100.5),
129+
},
130+
)
131+
```
132+
133+
### Logging
134+
135+
To see which method is being used, enable trace-level logging:
136+
137+
```python
138+
import logging
139+
140+
logging.basicConfig(level=logging.TRACE)
141+
logging.getLogger("rasterix").setLevel(logging.TRACE)
142+
```
143+
144+
This will output messages like:
145+
146+
```
147+
TRACE:rasterix:Creating affine from STAC proj:transform attribute
148+
```

docs/raster_index/crs.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
},
4949
"outputs": [],
5050
"source": [
51-
"source = \"/vsicurl/https://noaadata.apps.nsidc.org/NOAA/G02135/south/daily/geotiff/2024/01_Jan/S_20240101_concentration_v3.0.tif\""
51+
"source = \"/vsicurl/https://noaadata.apps.nsidc.org/NOAA/G02135/south/daily/geotiff/2024/01_Jan/S_20240101_concentration_v4.0.tif\""
5252
]
5353
},
5454
{

docs/raster_index/indexing.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
},
7171
"outputs": [],
7272
"source": [
73-
"source = \"https://noaadata.apps.nsidc.org/NOAA/G02135/south/daily/geotiff/2024/01_Jan/S_20240101_concentration_v3.0.tif\"\n",
73+
"source = \"https://noaadata.apps.nsidc.org/NOAA/G02135/south/daily/geotiff/2024/01_Jan/S_20240101_concentration_v4.0.tif\"\n",
7474
"\n",
7575
"da = xr.open_dataarray(source, engine=\"rasterio\").pipe(rasterix.assign_index)\n",
7676
"da"

docs/raster_index/intro.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181
},
8282
"outputs": [],
8383
"source": [
84-
"source = \"https://noaadata.apps.nsidc.org/NOAA/G02135/south/daily/geotiff/2024/01_Jan/S_20240101_concentration_v3.0.tif\""
84+
"source = \"https://noaadata.apps.nsidc.org/NOAA/G02135/south/daily/geotiff/2024/01_Jan/S_20240101_concentration_v4.0.tif\""
8585
]
8686
},
8787
{

0 commit comments

Comments
 (0)