tessera-eval 1.11.0__tar.gz
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.
- tessera_eval-1.11.0/LICENSE +21 -0
- tessera_eval-1.11.0/PKG-INFO +278 -0
- tessera_eval-1.11.0/README.md +238 -0
- tessera_eval-1.11.0/pyproject.toml +70 -0
- tessera_eval-1.11.0/setup.cfg +4 -0
- tessera_eval-1.11.0/tessera_eval/__init__.py +44 -0
- tessera_eval-1.11.0/tessera_eval/classify.py +315 -0
- tessera_eval-1.11.0/tessera_eval/cli.py +766 -0
- tessera_eval-1.11.0/tessera_eval/data.py +503 -0
- tessera_eval-1.11.0/tessera_eval/evaluate.py +1118 -0
- tessera_eval-1.11.0/tessera_eval/rasterize.py +187 -0
- tessera_eval-1.11.0/tessera_eval/server.py +3598 -0
- tessera_eval-1.11.0/tessera_eval/unet.py +720 -0
- tessera_eval-1.11.0/tessera_eval.egg-info/PKG-INFO +278 -0
- tessera_eval-1.11.0/tessera_eval.egg-info/SOURCES.txt +52 -0
- tessera_eval-1.11.0/tessera_eval.egg-info/dependency_links.txt +1 -0
- tessera_eval-1.11.0/tessera_eval.egg-info/entry_points.txt +3 -0
- tessera_eval-1.11.0/tessera_eval.egg-info/requires.txt +34 -0
- tessera_eval-1.11.0/tessera_eval.egg-info/top_level.txt +1 -0
- tessera_eval-1.11.0/tests/test_align_raster_sentinels.py +123 -0
- tessera_eval-1.11.0/tests/test_cli.py +465 -0
- tessera_eval-1.11.0/tests/test_compute_proxy.py +118 -0
- tessera_eval-1.11.0/tests/test_create_map_compression.py +78 -0
- tessera_eval-1.11.0/tests/test_create_map_multi_tile_crs.py +219 -0
- tessera_eval-1.11.0/tests/test_create_map_preview.py +97 -0
- tessera_eval-1.11.0/tests/test_create_map_regression.py +281 -0
- tessera_eval-1.11.0/tests/test_create_map_unique_download_url.py +100 -0
- tessera_eval-1.11.0/tests/test_create_map_year.py +148 -0
- tessera_eval-1.11.0/tests/test_evaluate_wrapper.py +48 -0
- tessera_eval-1.11.0/tests/test_extract_tile_patches_memory.py +91 -0
- tessera_eval-1.11.0/tests/test_extract_tile_patches_regression.py +152 -0
- tessera_eval-1.11.0/tests/test_fit_heartbeat.py +137 -0
- tessera_eval-1.11.0/tests/test_kfold.py +390 -0
- tessera_eval-1.11.0/tests/test_list_shapefiles.py +49 -0
- tessera_eval-1.11.0/tests/test_load_raster.py +99 -0
- tessera_eval-1.11.0/tests/test_map_task_type_persisted_early.py +253 -0
- tessera_eval-1.11.0/tests/test_predict_raster_clamp.py +90 -0
- tessera_eval-1.11.0/tests/test_rasterize_encoder.py +105 -0
- tessera_eval-1.11.0/tests/test_run_large_area_kfold.py +233 -0
- tessera_eval-1.11.0/tests/test_run_large_area_regression.py +194 -0
- tessera_eval-1.11.0/tests/test_run_large_area_test_file.py +187 -0
- tessera_eval-1.11.0/tests/test_run_learning_curve_regression.py +339 -0
- tessera_eval-1.11.0/tests/test_sample_points_within_budget.py +78 -0
- tessera_eval-1.11.0/tests/test_seed_threading.py +137 -0
- tessera_eval-1.11.0/tests/test_server_zarr.py +89 -0
- tessera_eval-1.11.0/tests/test_spatial_split.py +165 -0
- tessera_eval-1.11.0/tests/test_start_event_task_on_cache_hit.py +111 -0
- tessera_eval-1.11.0/tests/test_tile_cache_reload.py +151 -0
- tessera_eval-1.11.0/tests/test_tile_disk_cache.py +74 -0
- tessera_eval-1.11.0/tests/test_train_models_regression.py +122 -0
- tessera_eval-1.11.0/tests/test_train_test_year_split.py +173 -0
- tessera_eval-1.11.0/tests/test_unet_regression.py +132 -0
- tessera_eval-1.11.0/tests/test_vq_loader.py +124 -0
- tessera_eval-1.11.0/tests/test_xgboost_label_gap.py +145 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 S. Keshav and contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tessera-eval
|
|
3
|
+
Version: 1.11.0
|
|
4
|
+
Summary: Evaluate habitat classifiers on Tessera satellite embeddings
|
|
5
|
+
Author: S. Keshav
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ucam-eo/tessera-eval
|
|
8
|
+
Project-URL: Repository, https://github.com/ucam-eo/tessera-eval
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: numpy>=1.24
|
|
13
|
+
Requires-Dist: geopandas>=0.14
|
|
14
|
+
Requires-Dist: rasterio>=1.3
|
|
15
|
+
Requires-Dist: scikit-learn>=1.3
|
|
16
|
+
Requires-Dist: affine>=2.4
|
|
17
|
+
Requires-Dist: typer>=0.12
|
|
18
|
+
Provides-Extra: geotessera
|
|
19
|
+
Requires-Dist: geotessera>=0.10.1; extra == "geotessera"
|
|
20
|
+
Provides-Extra: xgboost
|
|
21
|
+
Requires-Dist: xgboost>=1.7; extra == "xgboost"
|
|
22
|
+
Provides-Extra: torch
|
|
23
|
+
Requires-Dist: torch>=2.0; extra == "torch"
|
|
24
|
+
Provides-Extra: plot
|
|
25
|
+
Requires-Dist: matplotlib>=3.5; extra == "plot"
|
|
26
|
+
Provides-Extra: server
|
|
27
|
+
Requires-Dist: flask>=3.0; extra == "server"
|
|
28
|
+
Requires-Dist: waitress>=2.1; extra == "server"
|
|
29
|
+
Requires-Dist: requests>=2.28; extra == "server"
|
|
30
|
+
Requires-Dist: geotessera>=0.10.1; extra == "server"
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
33
|
+
Requires-Dist: ruff<0.17,>=0.16; extra == "dev"
|
|
34
|
+
Requires-Dist: mypy>=1.8; extra == "dev"
|
|
35
|
+
Provides-Extra: all
|
|
36
|
+
Requires-Dist: geotessera>=0.10.1; extra == "all"
|
|
37
|
+
Requires-Dist: xgboost>=1.7; extra == "all"
|
|
38
|
+
Requires-Dist: matplotlib>=3.5; extra == "all"
|
|
39
|
+
Dynamic: license-file
|
|
40
|
+
|
|
41
|
+
# tessera-eval
|
|
42
|
+
|
|
43
|
+
Evaluate land-cover / habitat classifiers on **Tessera** satellite embeddings.
|
|
44
|
+
|
|
45
|
+
[Tessera](https://github.com/ucam-eo/tessera) is a geospatial foundation model
|
|
46
|
+
that produces a **128-dimensional embedding for every ~10 m pixel** of the Earth's
|
|
47
|
+
surface, per year. `tessera-eval` is a small, framework-independent Python library
|
|
48
|
+
for the question that immediately follows: *how well can you map a category of
|
|
49
|
+
interest (a habitat, crop, or land-cover class) from those embeddings, given some
|
|
50
|
+
labelled polygons?*
|
|
51
|
+
|
|
52
|
+
It handles the unglamorous-but-fiddly parts end to end:
|
|
53
|
+
|
|
54
|
+
- **Loading + dequantizing** embeddings from the formats Tessera tooling emits
|
|
55
|
+
(GeoTessera `int8 × per-pixel-scale` tiles, and TEE per-dim `uint8` vector
|
|
56
|
+
directories).
|
|
57
|
+
- **Rasterizing** a labelled shapefile/GeoJSON onto the embedding pixel grid with
|
|
58
|
+
stable class IDs.
|
|
59
|
+
- **Training + scoring** a panel of classifiers/regressors (k-NN, random forest,
|
|
60
|
+
MLP, spatial MLP, optional XGBoost, optional U-Net) with **learning curves**,
|
|
61
|
+
**k-fold cross-validation**, and **spatial hold-out** splits.
|
|
62
|
+
- An optional **local compute server** (`tee-compute`) so you can run the ML on
|
|
63
|
+
your own machine while pulling tiles/UI from a hosted service.
|
|
64
|
+
|
|
65
|
+
> The library core (`data`, `rasterize`, `classify`, `evaluate`) is pure NumPy /
|
|
66
|
+
> scikit-learn / rasterio and has no web-framework or hosting dependency. The
|
|
67
|
+
> compute server reads zarr embeddings through geotessera's own `GeoTesseraZarr`
|
|
68
|
+
> interface, falling back to NPY tiles when the store lacks coverage.
|
|
69
|
+
|
|
70
|
+
## Install
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pip install tessera-eval # core library
|
|
74
|
+
pip install "tessera-eval[geotessera]" # + tile access (load_embeddings_for_shapefile)
|
|
75
|
+
pip install "tessera-eval[server]" # + the tee-compute local server
|
|
76
|
+
pip install "tessera-eval[all]" # geotessera + xgboost + matplotlib
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Optional extras: `geotessera` (fetch tiles), `xgboost` (gradient-boosted models),
|
|
80
|
+
`torch` (the U-Net), `plot` (matplotlib), `server` (Flask compute server),
|
|
81
|
+
`dev` (pytest/ruff/mypy). Python ≥ 3.10.
|
|
82
|
+
|
|
83
|
+
## Quickstart
|
|
84
|
+
|
|
85
|
+
Cross-validate a classifier on labelled polygons, pulling embeddings tile-by-tile:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
import geopandas as gpd
|
|
89
|
+
from geotessera import GeoTessera
|
|
90
|
+
from tessera_eval import load_embeddings_for_shapefile, run_kfold_cv
|
|
91
|
+
|
|
92
|
+
# 1. Labelled polygons (any CRS — reprojected internally) with a class column.
|
|
93
|
+
gdf = gpd.read_file("habitats.geojson")
|
|
94
|
+
|
|
95
|
+
# 2. Pull a 128-d embedding for every pixel under the polygons (memory-bounded:
|
|
96
|
+
# one GeoTessera tile at a time, keeping only labelled pixels).
|
|
97
|
+
gt = GeoTessera()
|
|
98
|
+
vectors, labels, class_names, stats = load_embeddings_for_shapefile(
|
|
99
|
+
gdf, field="habitat", year=2024, gt_instance=gt
|
|
100
|
+
)
|
|
101
|
+
print(f"{stats['total_pixels']:,} labelled pixels over {stats['n_classes']} classes")
|
|
102
|
+
|
|
103
|
+
# 3. 5-fold cross-validation of a random forest and a nearest-neighbour baseline.
|
|
104
|
+
for event in run_kfold_cv(vectors, labels, ["rf", "nn"], k=5):
|
|
105
|
+
if event["type"] == "aggregate":
|
|
106
|
+
for name, m in event["models"].items():
|
|
107
|
+
print(f"{name:>4}: macro-F1 {m['mean_f1']:.3f} ± {m['std_f1']:.3f}")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Already have a TEE vector directory on disk? Load it directly:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from tessera_eval import load_tee_vectors
|
|
114
|
+
|
|
115
|
+
vectors, coords, metadata = load_tee_vectors("/path/to/vectors/aoi/2024")
|
|
116
|
+
# vectors: float32 (N, 128); coords: int32 (N, 2) pixel (x, y); metadata: dict
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
See the [tutorial](docs/tutorial.md) for the full workflow (labels → learning
|
|
120
|
+
curve → confusion matrix → interpretation).
|
|
121
|
+
|
|
122
|
+
## Command-line interface
|
|
123
|
+
|
|
124
|
+
The workflow covered in the [tutorial](docs/tutorial.md) can also be run through the command line.
|
|
125
|
+
|
|
126
|
+
First, install the `geotessera` package needed for the `load` step below:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
pip install -e ".[geotessera]"
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Optional installation for using xgboost:
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
pip install -e ".[xgboost]"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Download the Tessera embeddings for your labelled ground truth, and save the result to a file (vectors.npz by default, change the name with argument `--output`). `--data` accepts either a shapefile/GeoJSON of labelled polygons or a GeoTIFF of an already-rasterized reference layer.
|
|
139
|
+
|
|
140
|
+
For a shapefile/GeoJSON, `--field` is the column holding the class or target values (e.g. `habitat`):
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
tessera-eval load --data /path/to/habitats.geojson --field habitat --year 2024
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
For a GeoTIFF, `--bbox` is required, in EPSG:4326 (longitude,latitude in degrees), since a raster has no natural area boundary the way labelled polygons do. `--nodata` marks any missing-value codes:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
tessera-eval load --data site_type.tif --bbox 27.1,67.75,27.2,67.85 --year 2024 --nodata 32766,32767
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`kfold` and `learning-curve` reuse the cached `vectors.npz` automatically. Pass `--vectors <path>` to use a different cached file instead.
|
|
153
|
+
|
|
154
|
+
Run k-fold cross-validation and print accuracy per model.
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
tessera-eval kfold --models rf,nn,mlp # for classification
|
|
158
|
+
tessera-eval kfold --models rf_reg,nn_reg # for regression
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Optional arguments:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
# --k: number of cross-validation folds (default: 5)
|
|
165
|
+
# --seed: random seed for reproducible fold splits (default: 42)
|
|
166
|
+
# --confusion-matrix: also print the full confusion matrix (raw counts), on top of the summary shown by default (for classification only)
|
|
167
|
+
# --vectors: path to a different cached .npz (default: vectors.npz from `load`)
|
|
168
|
+
# --max-samples: cap the training set size per fold (random, not stratified by class) -
|
|
169
|
+
# usually needed for raster-derived data, which labels every pixel, not a hand-picked subset
|
|
170
|
+
tessera-eval kfold --models rf --k 10 --seed 1 --confusion-matrix --max-samples 50000
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Investigate how accuracy changes using different fractions of training labels (currently only for classification task).
|
|
174
|
+
```bash
|
|
175
|
+
tessera-eval learning-curve --models rf --training-pcts 1,5,10,30,50,80 --repeats 5 # --training-pcts: % of labels per step (default: 1,5,10,30,50,80); --repeats: random repeats per step (default: 5)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
Since neighbouring pixels are usually very similar, a random split can overstate how accurate the model really is (see [tutorial](docs/tutorial.md)). For a more reliable estimate, train on one geographic half of your area and test on the other, splitting by longitude:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
tessera-eval learning-curve --models rf --spatial-holdout
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
You can also choose exactly which region to hold out, by passing a bounding box (for a shapefile/GeoJSON, in the same CRS as your data; for a GeoTIFF, always EPSG:4326):
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
tessera-eval learning-curve --models rf --spatial-holdout --test-bbox 27.16,67.77,27.23,67.82
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
You can also use a completely separate file as the test set - a different region, a different year, or both. If you are evaluating on a different year, pass `--test-year` along with `--test-data`.
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
tessera-eval learning-curve --models rf --test-data /path/to/other_region.geojson --test-year 2023
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
For a GeoTIFF, `--test-bbox` is also required, defining the test region within that file:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
tessera-eval learning-curve --test-data site_type_2023.tif --test-bbox 27.1,67.75,27.2,67.85 --test-year 2023 --models rf
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
This reuses the cached `vectors.npz` as the training data by default. To use a different training area or year instead, pass `--data`/`--bbox`/`--year` explicitly:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
tessera-eval learning-curve --data site_type_2019.tif --bbox 27.1,67.75,27.2,67.85 --year 2019 --test-data site_type_2023.tif --test-bbox 27.1,67.75,27.2,67.85 --test-year 2023 --models rf
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Run any command with `--help` for a list of all possible arguments.
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
## Documentation
|
|
216
|
+
|
|
217
|
+
- **[Data formats](docs/data-formats.md)** — the Tessera embedding formats this
|
|
218
|
+
library reads and the exact dequantization maths. *Start here if you're wiring in
|
|
219
|
+
your own data.*
|
|
220
|
+
- **[API reference](docs/api-reference.md)** — every public function, with array
|
|
221
|
+
shapes and dtypes.
|
|
222
|
+
- **[Tutorial](docs/tutorial.md)** — an end-to-end worked example.
|
|
223
|
+
- **[Compute server](docs/compute-server.md)** — running `tee-compute` (local ML,
|
|
224
|
+
hosted data).
|
|
225
|
+
|
|
226
|
+
## What's in the box
|
|
227
|
+
|
|
228
|
+
| Module | Purpose |
|
|
229
|
+
|---|---|
|
|
230
|
+
| `tessera_eval.data` | Load + dequantize embeddings (`load_tee_vectors`, `dequantize_int8`, `dequantize_uint8`, `load_embeddings_for_shapefile`, `load_embeddings_for_shapefile_vq`, `load_embeddings_for_raster`). |
|
|
231
|
+
| `tessera_eval.rasterize` | Burn shapefile polygons onto a pixel grid with stable, 1-based class IDs|
|
|
232
|
+
| `tessera_eval.classify` | Classifier/regressor factory + spatial neighbourhood features. |
|
|
233
|
+
| `tessera_eval.evaluate` | Learning curves, k-fold CV, spatial split, metrics, field-type detection. |
|
|
234
|
+
| `tessera_eval.unet` | Optional PyTorch U-Net for sparse-label tile segmentation. |
|
|
235
|
+
| `tessera_eval.server` | `tee-compute`: local Flask compute server, proxies data/UI to a hosted TEE. |
|
|
236
|
+
| `tessera_eval.cli` | `tessera-eval` command-line interface: `load`, `kfold`, `learning-curve`. |
|
|
237
|
+
|
|
238
|
+
The compute server reads zarr embeddings directly through geotessera's
|
|
239
|
+
`GeoTesseraZarr` interface as a fast path, probing coverage first and falling
|
|
240
|
+
back to NPY tiles whenever the store is unavailable or lacks the requested
|
|
241
|
+
region or year.
|
|
242
|
+
|
|
243
|
+
Available models: `nn`, `rf`, `mlp`, `spatial_mlp`, `spatial_mlp_5x5`, `xgboost`
|
|
244
|
+
(if installed), `unet` (if torch installed); regressors `nn_reg`, `rf_reg`,
|
|
245
|
+
`mlp_reg`, `xgboost_reg`. See `available_classifiers()` / `available_regressors()`.
|
|
246
|
+
|
|
247
|
+
## Design notes
|
|
248
|
+
|
|
249
|
+
- **Class imbalance is expected and fine.** Macro-F1 is reported alongside
|
|
250
|
+
weighted-F1 precisely so rare classes are visible.
|
|
251
|
+
- **Determinism.** Estimators use `random_state=42`; evaluation takes an explicit
|
|
252
|
+
`seed`. Same inputs → same numbers.
|
|
253
|
+
- **Spatial leakage.** For honest accuracy on contiguous habitats, prefer the
|
|
254
|
+
spatial hold-out (`run_learning_curve(..., test_vectors=, test_labels=)`) over a
|
|
255
|
+
random pixel split — neighbouring pixels are highly autocorrelated.
|
|
256
|
+
- **Memory.** `load_embeddings_for_shapefile` streams one tile at a time and keeps
|
|
257
|
+
only labelled pixels, so county/country-scale shapefiles are tractable.
|
|
258
|
+
|
|
259
|
+
## Development
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
git clone https://github.com/ucam-eo/tessera-eval && cd tessera-eval
|
|
263
|
+
python -m venv .venv && source .venv/bin/activate
|
|
264
|
+
pip install -e ".[server,dev]"
|
|
265
|
+
ruff check . && ruff format --check . && pytest
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
269
|
+
|
|
270
|
+
## Citing
|
|
271
|
+
|
|
272
|
+
If this is useful in academic work, please cite the Tessera model and link back to
|
|
273
|
+
this repository. (A `CITATION.cff` will be added alongside the Tessera paper
|
|
274
|
+
reference.)
|
|
275
|
+
|
|
276
|
+
## License
|
|
277
|
+
|
|
278
|
+
[MIT](LICENSE).
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# tessera-eval
|
|
2
|
+
|
|
3
|
+
Evaluate land-cover / habitat classifiers on **Tessera** satellite embeddings.
|
|
4
|
+
|
|
5
|
+
[Tessera](https://github.com/ucam-eo/tessera) is a geospatial foundation model
|
|
6
|
+
that produces a **128-dimensional embedding for every ~10 m pixel** of the Earth's
|
|
7
|
+
surface, per year. `tessera-eval` is a small, framework-independent Python library
|
|
8
|
+
for the question that immediately follows: *how well can you map a category of
|
|
9
|
+
interest (a habitat, crop, or land-cover class) from those embeddings, given some
|
|
10
|
+
labelled polygons?*
|
|
11
|
+
|
|
12
|
+
It handles the unglamorous-but-fiddly parts end to end:
|
|
13
|
+
|
|
14
|
+
- **Loading + dequantizing** embeddings from the formats Tessera tooling emits
|
|
15
|
+
(GeoTessera `int8 × per-pixel-scale` tiles, and TEE per-dim `uint8` vector
|
|
16
|
+
directories).
|
|
17
|
+
- **Rasterizing** a labelled shapefile/GeoJSON onto the embedding pixel grid with
|
|
18
|
+
stable class IDs.
|
|
19
|
+
- **Training + scoring** a panel of classifiers/regressors (k-NN, random forest,
|
|
20
|
+
MLP, spatial MLP, optional XGBoost, optional U-Net) with **learning curves**,
|
|
21
|
+
**k-fold cross-validation**, and **spatial hold-out** splits.
|
|
22
|
+
- An optional **local compute server** (`tee-compute`) so you can run the ML on
|
|
23
|
+
your own machine while pulling tiles/UI from a hosted service.
|
|
24
|
+
|
|
25
|
+
> The library core (`data`, `rasterize`, `classify`, `evaluate`) is pure NumPy /
|
|
26
|
+
> scikit-learn / rasterio and has no web-framework or hosting dependency. The
|
|
27
|
+
> compute server reads zarr embeddings through geotessera's own `GeoTesseraZarr`
|
|
28
|
+
> interface, falling back to NPY tiles when the store lacks coverage.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install tessera-eval # core library
|
|
34
|
+
pip install "tessera-eval[geotessera]" # + tile access (load_embeddings_for_shapefile)
|
|
35
|
+
pip install "tessera-eval[server]" # + the tee-compute local server
|
|
36
|
+
pip install "tessera-eval[all]" # geotessera + xgboost + matplotlib
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Optional extras: `geotessera` (fetch tiles), `xgboost` (gradient-boosted models),
|
|
40
|
+
`torch` (the U-Net), `plot` (matplotlib), `server` (Flask compute server),
|
|
41
|
+
`dev` (pytest/ruff/mypy). Python ≥ 3.10.
|
|
42
|
+
|
|
43
|
+
## Quickstart
|
|
44
|
+
|
|
45
|
+
Cross-validate a classifier on labelled polygons, pulling embeddings tile-by-tile:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import geopandas as gpd
|
|
49
|
+
from geotessera import GeoTessera
|
|
50
|
+
from tessera_eval import load_embeddings_for_shapefile, run_kfold_cv
|
|
51
|
+
|
|
52
|
+
# 1. Labelled polygons (any CRS — reprojected internally) with a class column.
|
|
53
|
+
gdf = gpd.read_file("habitats.geojson")
|
|
54
|
+
|
|
55
|
+
# 2. Pull a 128-d embedding for every pixel under the polygons (memory-bounded:
|
|
56
|
+
# one GeoTessera tile at a time, keeping only labelled pixels).
|
|
57
|
+
gt = GeoTessera()
|
|
58
|
+
vectors, labels, class_names, stats = load_embeddings_for_shapefile(
|
|
59
|
+
gdf, field="habitat", year=2024, gt_instance=gt
|
|
60
|
+
)
|
|
61
|
+
print(f"{stats['total_pixels']:,} labelled pixels over {stats['n_classes']} classes")
|
|
62
|
+
|
|
63
|
+
# 3. 5-fold cross-validation of a random forest and a nearest-neighbour baseline.
|
|
64
|
+
for event in run_kfold_cv(vectors, labels, ["rf", "nn"], k=5):
|
|
65
|
+
if event["type"] == "aggregate":
|
|
66
|
+
for name, m in event["models"].items():
|
|
67
|
+
print(f"{name:>4}: macro-F1 {m['mean_f1']:.3f} ± {m['std_f1']:.3f}")
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Already have a TEE vector directory on disk? Load it directly:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from tessera_eval import load_tee_vectors
|
|
74
|
+
|
|
75
|
+
vectors, coords, metadata = load_tee_vectors("/path/to/vectors/aoi/2024")
|
|
76
|
+
# vectors: float32 (N, 128); coords: int32 (N, 2) pixel (x, y); metadata: dict
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
See the [tutorial](docs/tutorial.md) for the full workflow (labels → learning
|
|
80
|
+
curve → confusion matrix → interpretation).
|
|
81
|
+
|
|
82
|
+
## Command-line interface
|
|
83
|
+
|
|
84
|
+
The workflow covered in the [tutorial](docs/tutorial.md) can also be run through the command line.
|
|
85
|
+
|
|
86
|
+
First, install the `geotessera` package needed for the `load` step below:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
pip install -e ".[geotessera]"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Optional installation for using xgboost:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
pip install -e ".[xgboost]"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Download the Tessera embeddings for your labelled ground truth, and save the result to a file (vectors.npz by default, change the name with argument `--output`). `--data` accepts either a shapefile/GeoJSON of labelled polygons or a GeoTIFF of an already-rasterized reference layer.
|
|
99
|
+
|
|
100
|
+
For a shapefile/GeoJSON, `--field` is the column holding the class or target values (e.g. `habitat`):
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
tessera-eval load --data /path/to/habitats.geojson --field habitat --year 2024
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
For a GeoTIFF, `--bbox` is required, in EPSG:4326 (longitude,latitude in degrees), since a raster has no natural area boundary the way labelled polygons do. `--nodata` marks any missing-value codes:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
tessera-eval load --data site_type.tif --bbox 27.1,67.75,27.2,67.85 --year 2024 --nodata 32766,32767
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`kfold` and `learning-curve` reuse the cached `vectors.npz` automatically. Pass `--vectors <path>` to use a different cached file instead.
|
|
113
|
+
|
|
114
|
+
Run k-fold cross-validation and print accuracy per model.
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
tessera-eval kfold --models rf,nn,mlp # for classification
|
|
118
|
+
tessera-eval kfold --models rf_reg,nn_reg # for regression
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Optional arguments:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# --k: number of cross-validation folds (default: 5)
|
|
125
|
+
# --seed: random seed for reproducible fold splits (default: 42)
|
|
126
|
+
# --confusion-matrix: also print the full confusion matrix (raw counts), on top of the summary shown by default (for classification only)
|
|
127
|
+
# --vectors: path to a different cached .npz (default: vectors.npz from `load`)
|
|
128
|
+
# --max-samples: cap the training set size per fold (random, not stratified by class) -
|
|
129
|
+
# usually needed for raster-derived data, which labels every pixel, not a hand-picked subset
|
|
130
|
+
tessera-eval kfold --models rf --k 10 --seed 1 --confusion-matrix --max-samples 50000
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Investigate how accuracy changes using different fractions of training labels (currently only for classification task).
|
|
134
|
+
```bash
|
|
135
|
+
tessera-eval learning-curve --models rf --training-pcts 1,5,10,30,50,80 --repeats 5 # --training-pcts: % of labels per step (default: 1,5,10,30,50,80); --repeats: random repeats per step (default: 5)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
Since neighbouring pixels are usually very similar, a random split can overstate how accurate the model really is (see [tutorial](docs/tutorial.md)). For a more reliable estimate, train on one geographic half of your area and test on the other, splitting by longitude:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
tessera-eval learning-curve --models rf --spatial-holdout
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
You can also choose exactly which region to hold out, by passing a bounding box (for a shapefile/GeoJSON, in the same CRS as your data; for a GeoTIFF, always EPSG:4326):
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
tessera-eval learning-curve --models rf --spatial-holdout --test-bbox 27.16,67.77,27.23,67.82
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
You can also use a completely separate file as the test set - a different region, a different year, or both. If you are evaluating on a different year, pass `--test-year` along with `--test-data`.
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
tessera-eval learning-curve --models rf --test-data /path/to/other_region.geojson --test-year 2023
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
For a GeoTIFF, `--test-bbox` is also required, defining the test region within that file:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
tessera-eval learning-curve --test-data site_type_2023.tif --test-bbox 27.1,67.75,27.2,67.85 --test-year 2023 --models rf
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
This reuses the cached `vectors.npz` as the training data by default. To use a different training area or year instead, pass `--data`/`--bbox`/`--year` explicitly:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
tessera-eval learning-curve --data site_type_2019.tif --bbox 27.1,67.75,27.2,67.85 --year 2019 --test-data site_type_2023.tif --test-bbox 27.1,67.75,27.2,67.85 --test-year 2023 --models rf
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Run any command with `--help` for a list of all possible arguments.
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
## Documentation
|
|
176
|
+
|
|
177
|
+
- **[Data formats](docs/data-formats.md)** — the Tessera embedding formats this
|
|
178
|
+
library reads and the exact dequantization maths. *Start here if you're wiring in
|
|
179
|
+
your own data.*
|
|
180
|
+
- **[API reference](docs/api-reference.md)** — every public function, with array
|
|
181
|
+
shapes and dtypes.
|
|
182
|
+
- **[Tutorial](docs/tutorial.md)** — an end-to-end worked example.
|
|
183
|
+
- **[Compute server](docs/compute-server.md)** — running `tee-compute` (local ML,
|
|
184
|
+
hosted data).
|
|
185
|
+
|
|
186
|
+
## What's in the box
|
|
187
|
+
|
|
188
|
+
| Module | Purpose |
|
|
189
|
+
|---|---|
|
|
190
|
+
| `tessera_eval.data` | Load + dequantize embeddings (`load_tee_vectors`, `dequantize_int8`, `dequantize_uint8`, `load_embeddings_for_shapefile`, `load_embeddings_for_shapefile_vq`, `load_embeddings_for_raster`). |
|
|
191
|
+
| `tessera_eval.rasterize` | Burn shapefile polygons onto a pixel grid with stable, 1-based class IDs|
|
|
192
|
+
| `tessera_eval.classify` | Classifier/regressor factory + spatial neighbourhood features. |
|
|
193
|
+
| `tessera_eval.evaluate` | Learning curves, k-fold CV, spatial split, metrics, field-type detection. |
|
|
194
|
+
| `tessera_eval.unet` | Optional PyTorch U-Net for sparse-label tile segmentation. |
|
|
195
|
+
| `tessera_eval.server` | `tee-compute`: local Flask compute server, proxies data/UI to a hosted TEE. |
|
|
196
|
+
| `tessera_eval.cli` | `tessera-eval` command-line interface: `load`, `kfold`, `learning-curve`. |
|
|
197
|
+
|
|
198
|
+
The compute server reads zarr embeddings directly through geotessera's
|
|
199
|
+
`GeoTesseraZarr` interface as a fast path, probing coverage first and falling
|
|
200
|
+
back to NPY tiles whenever the store is unavailable or lacks the requested
|
|
201
|
+
region or year.
|
|
202
|
+
|
|
203
|
+
Available models: `nn`, `rf`, `mlp`, `spatial_mlp`, `spatial_mlp_5x5`, `xgboost`
|
|
204
|
+
(if installed), `unet` (if torch installed); regressors `nn_reg`, `rf_reg`,
|
|
205
|
+
`mlp_reg`, `xgboost_reg`. See `available_classifiers()` / `available_regressors()`.
|
|
206
|
+
|
|
207
|
+
## Design notes
|
|
208
|
+
|
|
209
|
+
- **Class imbalance is expected and fine.** Macro-F1 is reported alongside
|
|
210
|
+
weighted-F1 precisely so rare classes are visible.
|
|
211
|
+
- **Determinism.** Estimators use `random_state=42`; evaluation takes an explicit
|
|
212
|
+
`seed`. Same inputs → same numbers.
|
|
213
|
+
- **Spatial leakage.** For honest accuracy on contiguous habitats, prefer the
|
|
214
|
+
spatial hold-out (`run_learning_curve(..., test_vectors=, test_labels=)`) over a
|
|
215
|
+
random pixel split — neighbouring pixels are highly autocorrelated.
|
|
216
|
+
- **Memory.** `load_embeddings_for_shapefile` streams one tile at a time and keeps
|
|
217
|
+
only labelled pixels, so county/country-scale shapefiles are tractable.
|
|
218
|
+
|
|
219
|
+
## Development
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
git clone https://github.com/ucam-eo/tessera-eval && cd tessera-eval
|
|
223
|
+
python -m venv .venv && source .venv/bin/activate
|
|
224
|
+
pip install -e ".[server,dev]"
|
|
225
|
+
ruff check . && ruff format --check . && pytest
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
229
|
+
|
|
230
|
+
## Citing
|
|
231
|
+
|
|
232
|
+
If this is useful in academic work, please cite the Tessera model and link back to
|
|
233
|
+
this repository. (A `CITATION.cff` will be added alongside the Tessera paper
|
|
234
|
+
reference.)
|
|
235
|
+
|
|
236
|
+
## License
|
|
237
|
+
|
|
238
|
+
[MIT](LICENSE).
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tessera-eval"
|
|
7
|
+
version = "1.11.0"
|
|
8
|
+
description = "Evaluate habitat classifiers on Tessera satellite embeddings"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "S. Keshav"},
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"numpy>=1.24",
|
|
17
|
+
"geopandas>=0.14",
|
|
18
|
+
"rasterio>=1.3",
|
|
19
|
+
"scikit-learn>=1.3",
|
|
20
|
+
"affine>=2.4",
|
|
21
|
+
"typer>=0.12",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
geotessera = ["geotessera>=0.10.1"]
|
|
26
|
+
xgboost = ["xgboost>=1.7"]
|
|
27
|
+
torch = ["torch>=2.0"]
|
|
28
|
+
plot = ["matplotlib>=3.5"]
|
|
29
|
+
server = [
|
|
30
|
+
"flask>=3.0",
|
|
31
|
+
"waitress>=2.1",
|
|
32
|
+
"requests>=2.28",
|
|
33
|
+
"geotessera>=0.10.1",
|
|
34
|
+
]
|
|
35
|
+
dev = ["pytest>=7.0", "ruff>=0.16,<0.17", "mypy>=1.8"]
|
|
36
|
+
all = ["geotessera>=0.10.1", "xgboost>=1.7", "matplotlib>=3.5"]
|
|
37
|
+
|
|
38
|
+
[project.scripts]
|
|
39
|
+
tee-compute = "tessera_eval.server:main"
|
|
40
|
+
tessera-eval = "tessera_eval.cli:main"
|
|
41
|
+
|
|
42
|
+
[project.urls]
|
|
43
|
+
Homepage = "https://github.com/ucam-eo/tessera-eval"
|
|
44
|
+
Repository = "https://github.com/ucam-eo/tessera-eval"
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.packages.find]
|
|
47
|
+
include = ["tessera_eval*"]
|
|
48
|
+
|
|
49
|
+
[tool.pytest.ini_options]
|
|
50
|
+
testpaths = ["tests"]
|
|
51
|
+
|
|
52
|
+
[tool.ruff]
|
|
53
|
+
line-length = 100
|
|
54
|
+
target-version = "py310"
|
|
55
|
+
|
|
56
|
+
[tool.ruff.lint]
|
|
57
|
+
# Conservative default set; the codebase predates linting, so keep it gentle.
|
|
58
|
+
select = ["E", "F", "W", "I"]
|
|
59
|
+
ignore = [
|
|
60
|
+
"E501", # long lines abound in docstrings/tables; not worth churn
|
|
61
|
+
"E402", # several modules deliberately order a logger/guard before imports
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
[tool.ruff.lint.per-file-ignores]
|
|
65
|
+
"__init__.py" = ["F401"] # re-exports define the public API
|
|
66
|
+
|
|
67
|
+
[tool.mypy]
|
|
68
|
+
python_version = "3.10"
|
|
69
|
+
ignore_missing_imports = true
|
|
70
|
+
warn_unused_ignores = false
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""tessera-eval: Evaluate habitat classifiers on Tessera satellite embeddings."""
|
|
2
|
+
|
|
3
|
+
# Must be set before numpy/scipy import to avoid OpenBLAS crash on >128-core machines.
|
|
4
|
+
# Use 1 thread per BLAS call — joblib handles higher-level parallelism in sklearn.
|
|
5
|
+
import os as _os
|
|
6
|
+
|
|
7
|
+
for _var in ("OPENBLAS_NUM_THREADS", "MKL_NUM_THREADS", "OMP_NUM_THREADS"):
|
|
8
|
+
if _var not in _os.environ:
|
|
9
|
+
_os.environ[_var] = "1"
|
|
10
|
+
|
|
11
|
+
# Single source of truth: the installed package metadata (pyproject version),
|
|
12
|
+
# so __version__ can't drift from the release tag.
|
|
13
|
+
from importlib.metadata import PackageNotFoundError as _PkgNotFound
|
|
14
|
+
from importlib.metadata import version as _pkg_version
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
__version__ = _pkg_version("tessera-eval")
|
|
18
|
+
except _PkgNotFound: # running from a source tree that isn't installed
|
|
19
|
+
__version__ = "0+unknown"
|
|
20
|
+
|
|
21
|
+
from tessera_eval.classify import (
|
|
22
|
+
available_classifiers,
|
|
23
|
+
available_regressors,
|
|
24
|
+
gather_spatial_features,
|
|
25
|
+
gather_spatial_features_2d,
|
|
26
|
+
make_classifier,
|
|
27
|
+
make_regressor,
|
|
28
|
+
)
|
|
29
|
+
from tessera_eval.data import (
|
|
30
|
+
dequantize_int8,
|
|
31
|
+
dequantize_uint8,
|
|
32
|
+
load_embeddings_for_raster,
|
|
33
|
+
load_embeddings_for_shapefile,
|
|
34
|
+
load_embeddings_for_shapefile_vq,
|
|
35
|
+
load_tee_vectors,
|
|
36
|
+
)
|
|
37
|
+
from tessera_eval.evaluate import (
|
|
38
|
+
detect_field_type,
|
|
39
|
+
evaluate,
|
|
40
|
+
regression_metrics,
|
|
41
|
+
run_kfold_cv,
|
|
42
|
+
run_learning_curve,
|
|
43
|
+
)
|
|
44
|
+
from tessera_eval.rasterize import align_raster_to_grid, rasterize_shapefile
|