|
| 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 | +``` |
0 commit comments