Engines¶
Every engine subclasses DelineationEngine and is resolved by name with
get_engine. See Delineation engines for an
overview.
engines ¶
Delineation engines for agricultural field boundary detection.
Each engine wraps a different model or approach for extracting field
boundary polygons from satellite imagery or embeddings. Engine modules are
imported lazily by :func:get_engine, so importing this package does not
pull in torch or other optional dependencies.
DelineationEngine ¶
Bases: ABC
Abstract base class for delineation engines.
Subclasses must implement :meth:delineate. They may attach
JSON-serialisable run metadata (backend, model id, weights repository,
revision, sha256, thresholds, window dates, ...) to the returned frame as
gdf.attrs["engine_meta"]; the pipeline copies it into the provenance
record.
Source code in agribound/engines/base.py
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | |
delineate
abstractmethod
¶
Run field boundary delineation on a raster file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
raster_path
|
str
|
Path to the input GeoTIFF (composite or local file). |
required |
config
|
AgriboundConfig
|
Pipeline configuration. |
required |
Returns:
| Type | Description |
|---|---|
GeoDataFrame
|
Field boundary polygons with at minimum a |
Source code in agribound/engines/base.py
validate_input ¶
Validate that the input raster is compatible with this engine.
Checks that the raster has enough bands for the engine's
requires_bands (class attribute, falling back to the registry
entry): at least the highest 1-based index those canonical bands map
to for the configured source, with config.bands taking precedence
(:func:get_canonical_band_indices). For local rasters without
config.bands the indices are positional, so this is
len(requires_bands).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
raster_path
|
str
|
Path to the input raster. |
required |
config
|
AgriboundConfig
|
Pipeline configuration. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the input is incompatible. |
Source code in agribound/engines/base.py
prefetch
classmethod
¶
Download model weights so that inference can run offline.
Engines that load remote weights override this and return the local paths (or cache directories) they populated. The base implementation downloads nothing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
AgriboundConfig
|
Pipeline configuration (engine parameters select the model). |
required |
Returns:
| Type | Description |
|---|---|
list[str]
|
Local paths of the downloaded artefacts (empty here). |
Source code in agribound/engines/base.py
get_canonical_band_indices ¶
get_canonical_band_indices(source: str, canonical_names: list[str], bands: dict[str, int] | None = None) -> list[int]
Get 1-based raster band indices for canonical band names.
Looks up each canonical name ("R", "G", "B", "NIR",
"NIR_NARROW", "SWIR1", "SWIR2") in the source registry and
returns the corresponding 1-based band index in the composite written by
the source's builder.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
str
|
Satellite source name. |
required |
canonical_names
|
list[str]
|
Canonical band names to look up (e.g. |
required |
bands
|
dict[str, int] or None
|
Optional explicit mapping of canonical names to 1-based indices (for
example |
None
|
Returns:
| Type | Description |
|---|---|
list[int]
|
1-based band indices in the composite raster. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the source is unknown or a canonical band is not available. |
Notes
For source="local" without an explicit mapping the indices are
positional (1, 2, 3, ... in the order requested), i.e. the local file
is assumed to store the requested bands first and in that order.
Source code in agribound/engines/base.py
get_engine ¶
Factory function to get a delineation engine instance by name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
engine_name
|
str
|
Engine name (e.g. |
required |
Returns:
| Type | Description |
|---|---|
DelineationEngine
|
Engine instance. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the engine name is not recognised. |
Source code in agribound/engines/base.py
get_engine_class ¶
Import and return the engine class for engine_name without instantiating it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
engine_name
|
str
|
Engine name (e.g. |
required |
Returns:
| Type | Description |
|---|---|
type[DelineationEngine]
|
Engine class. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the engine name is not recognised. |
Source code in agribound/engines/base.py
list_engines ¶
List all delineation engines and their metadata.
Returns:
| Type | Description |
|---|---|
dict[str, dict]
|
Deep copy of :data: |
Examples:
>>> from agribound import list_engines
>>> for name, info in list_engines().items():
... print(name, info["approach"])
Source code in agribound/registry.py
Delineate-Anything¶
delineate_anything ¶
Delineate-Anything engine (YOLO11-seg instance segmentation of field boundaries).
Models
Weights come from the Hugging Face repository MykolaL/DelineateAnything at
pinned revisions; the SHA-256 of every downloaded file is checked against
:data:DA_MODELS before it is used.
large_v2(default):DelineateAnythingv2.pt, Delineate Anything v2, YOLO11x-seg trained on FBIS-73M; default confidence 0.15; FTW registry nameDelineateAnythingV2.large:DelineateAnything.pt, YOLO11x-seg trained on FBIS-22M; default confidence 0.005; FTW nameDelineateAnything.small:DelineateAnything-S.pt, YOLO11n-seg trained on FBIS-22M; default confidence 0.005; FTW nameDelineateAnything-S.
The default confidences are those of the upstream conf_sample.yaml: 0.15
for large_v2 (Lavreniuk/Delineate-Anything a6f30b2) and 0.005 for the v1
models (the v1-era sample configuration). Select a model with
engine_params["da_model"] (a key or one of the aliases
"DelineateAnythingV2", "DelineateAnything", "DelineateAnything-S");
the legacy engine_params["model_size"] ("large"/"small") selects
the v1 models.
Backends
engine_params["backend"] chooses the implementation explicitly (default
"native"). There is no automatic fallback between backends: a backend that
cannot run raises an error that says what is missing.
"native"
Agribound's own tiled Ultralytics inference. It reproduces the
preprocessing of the reference Delineate-Anything pipeline (DelAnyFlow,
upstream methods/main): a scene-level per-band 1-99 percentile stretch
to uint8, computed on valid, strictly positive pixels sampled
(nearest neighbour, full-resolution data, never overviews) on a grid of
at most 4096 px per side (uint8 rasters are used unchanged); tiles of 512
native pixels when the ground sampling distance (GSD) is below 4 m, else
256 native pixels upsampled 2x with bicubic interpolation, so the model
input is always 512 x 512 (super_resolution = 1, 2 or 4 overrides the
factor); 50 % tile overlap starting half a tile before the raster origin,
as the upstream ExecutionPlanner does; BGR channel order for NumPy
input to Ultralytics; retina_masks=True; masks cast to float before
the upstream 3 x 3 erode / dilate / dilate / erode morphology (Ultralytics
>= 8.3.217 returns uint8 masks, on which the upstream negation trick would
turn the erosion into a dilation); FP16 on GPU/MPS. Each detection becomes
the largest polygon of its mask, clipped to valid pixels, and is flagged
when it touches an interior tile edge (a tile-cut detection). The
detections of all tiles are then combined at polygon level, with
duplicates defined as IoU >= dedup_iou or intersection >=
dedup_containment of the smaller polygon: (1) tile-cut duplicates of
one another are merged into their union (:func:merge_tile_pieces;
merge_tile_pieces=False skips it), so a field too large to be
complete in any tile is rebuilt from its pieces (the polygon-level
counterpart of DelAnyFlow's merging of fields that touch a tile border);
pieces that duplicate a complete (not tile-cut), at least as large
detection are not merged; (2) greedy non-maximum suppression visits the
polygons by higher confidence, then larger area, and drops every polygon
that duplicates one already kept, except that a complete detection is
visited before each tile-cut duplicate that is not larger than it
(:func:deduplicate_detections); (3) remaining overlaps go to the
polygon visited first in that order (:func:resolve_overlaps;
resolve_overlaps=False keeps them). This is simpler than DelAnyFlow's
raster-level region merging (which, for example, lets smaller fields
carve their area out of larger ones), so results are close to, but not
identical with, the "reference" backend. NMS uses IoU 0.3 by
default, the value of the authors' openEO UDP
(openeo_udp/udf/delineate_onnx.py); the upstream execute() keeps
Ultralytics' default (0.7). The raster is read tile by tile.
"reference"
Runs the upstream DelAnyFlow pipeline (methods.main.inference.execute)
in a subprocess, from a Delineate-Anything checkout given by
engine_params["da_repo"] or the AGRIBOUND_DA_REPO environment
variable. Requires the GDAL Python bindings (osgeo) and a checkout that
contains the uint8-mask fix (upstream commit 34eddf7 or later).
"ftw"
ftw_tools.inference.inference.run_instance_segmentation. FTW's wrapper
divides the first three bands by 3000 (Sentinel-2 L2A units), clips to
[0, 1] and resizes bilinearly, so this backend accepts only
reflectance_x10000 composites. large_v2 needs an ftw-tools build
whose MODEL_REGISTRY contains DelineateAnythingV2 (ftw-baselines
main at fa86d4a or later; not in ftw-tools 2.0.0b5).
Engine parameters
All optional. A Delineate-Anything parameter that the selected backend cannot
honour raises :class:ValueError, as do the names confidence and
minimal_confidence (the confidence is conf_threshold for every
backend); parameters not listed here are left to other pipeline stages.
- All backends:
backend;da_model/model_size(large_v2);conf_threshold(per model; theftwbackend uses 0.15 for v2 and ftw-tools' 0.05 for v1);batch_size(tiles per forward pass, 4). nativeandreference:checkpoint_path(fine-tuned YOLO weights; set by the pipeline after fine-tuning);super_resolution(1, 2 or 4; default automatic);tile_step(fraction of the tile, 0.5);half(FP16 on GPU/MPS, True).nativeandftw:iou_threshold(NMS IoU, 0.3);max_detections(per tile, 300).native:dedup_iou(0.3),dedup_containment(0.8),merge_tile_pieces(True) andresolve_overlaps(True).reference:da_repo;min_hole_area_m2(holes smaller than this are filled, 2500 m², the upstreamconf_sample.yamlvalue).ftw:patch_size(256; a multiple of 32 smaller than the raster's smaller side),resize_factor(2),padding(FTW default),close_interiors(True),simplify(FTW simplification tolerance, applied in EPSG:6933 coordinates, i.e. metres that are exact only near 30° latitude; 0),max_size(m², None),overlap_iou_threshold(0.3),overlap_contain_threshold(0.8),value_scale(forlocalrasters).
config.min_field_area_m2 is applied as an absolute area in m², computed
in the equal-area EPSG:6933, by every backend (the reference pipeline's
automatic_area_scale is disabled; upstream and ftw-tools compute areas in
EPSG:6933 too). Holes: the native backend keeps holes of any size, the
reference backend fills holes smaller than min_hole_area_m2 and the
ftw backend fills all holes while close_interiors is True.
The returned frame carries gdf.attrs["engine_meta"] (backend, model key,
weights repository, revision and SHA-256, thresholds, super-resolution
factor, tile size, device, pixel size, ...); the native backend also
returns a confidence column. The published models were trained on
0.25-10 m imagery: for rasters outside that range (e.g. 30 m Landsat/HLS) a
WARNING is logged and engine_meta["gsd_outside_training_range"] is True
(:func:gsd_outside_training_range).
References
Lavreniuk, M., et al. (2025). Delineate Anything: Resolution-Agnostic Field Boundary Delineation on Satellite Imagery. European Conference on Artificial Intelligence (ECAI 2025). arXiv:2504.02534.
Lavreniuk, M., et al. (2025). Delineate Anything Flow: Fast, Country-Level Field Boundary Detection from Any Source. arXiv:2511.13417.
Lavreniuk, M., et al. (2026). Delineate Anything v2: A Global Foundation Model for Field Delineation. European Conference on Computer Vision Workshops (ECCVW 2026), GAIA workshop. arXiv:2607.19069.
Model code and weights are AGPL-3.0; Ultralytics is AGPL-3.0.
DA_MODELS
module-attribute
¶
DA_MODELS: dict[str, DAModel] = {'large_v2': DAModel(key='large_v2', filename='DelineateAnythingv2.pt', revision='369d0b4c44cf9bec2bd3a27bc81810cadd2c963e', sha256='46700b8a279b07922953a11adaeb5e658d9a2384b6334c8e0a3090886218915a', size_bytes=124747297, default_conf=0.15, ftw_name='DelineateAnythingV2', architecture='YOLO11x-seg', training_data='FBIS-73M'), 'large': DAModel(key='large', filename='DelineateAnything.pt', revision='029e9a94c6abc51c67cebdc9b9a9b6c1ac2b1187', sha256='e3dcda35780083aeaefe9425b73b15a30561cdc12c43277041d04f9e88ede029', size_bytes=124746842, default_conf=0.005, ftw_name='DelineateAnything', architecture='YOLO11x-seg', training_data='FBIS-22M'), 'small': DAModel(key='small', filename='DelineateAnything-S.pt', revision='029e9a94c6abc51c67cebdc9b9a9b6c1ac2b1187', sha256='5463cdfb73690fc506035e4f7dce26a4c06af6ef4d207570513110c4b879d643', size_bytes=17635629, default_conf=0.005, ftw_name='DelineateAnything-S', architecture='YOLO11n-seg', training_data='FBIS-22M')}
Pinned Delineate-Anything checkpoints (see the module docstring).
DelineateAnythingEngine ¶
Bases: DelineationEngine
Field boundary delineation with Delineate-Anything (see the module docstring).
Source code in agribound/engines/delineate_anything.py
1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 | |
delineate ¶
Run Delineate-Anything on a raster.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
raster_path
|
str
|
Input GeoTIFF (composite or local file). |
required |
config
|
AgriboundConfig
|
Pipeline configuration; |
required |
Returns:
| Type | Description |
|---|---|
GeoDataFrame
|
Field polygons in the raster CRS with
|
Source code in agribound/engines/delineate_anything.py
prefetch
classmethod
¶
Download the weights selected by config.engine_params.
native/reference: the pinned Hugging Face file (SHA-256
checked), unless checkpoint_path is set (then that file is
returned if it exists). ftw: ftw-tools' checkpoint URL, which
Ultralytics resolves relative to the current working directory, is
downloaded into the current working directory, so the later run must
start from the same directory to find it offline.
Returns:
| Type | Description |
|---|---|
list[str]
|
Local paths of the weights. |
Source code in agribound/engines/delineate_anything.py
Fields of The World¶
ftw ¶
FTW (Fields of The World) semantic-segmentation engine.
Runs an ftw-tools checkpoint on R, G, B and NIR and polygonises the predicted
field class (1) with ftw_tools.postprocess.polygonize.polygonize. The
default model is the ftw-tools MODEL_REGISTRY entry marked default
(FTW_PRUE_EFNET_B5 in ftw-tools 2.0.0b5: a PRUE U-Net with an
EfficientNet-B5 encoder, two input windows). Other registry models are chosen
with engine_params["model"] (see :func:list_ftw_models); a local
checkpoint with engine_params["checkpoint_path"].
Instance-segmentation entries of the registry (Delineate-Anything) are
rejected; use the delineate-anything engine for those.
Input windows
The number of windows follows the model: registry models use
ModelSpec.requires_window; for a checkpoint file in_channels is read
from its hyper_parameters (4 = one window, 8 = two windows).
Two-window models take [R, G, B, NIR] of an early-season window A
followed by the same bands of a late-season window B, the band order that
ftw-tools' own inference input builder writes (ftw inference download:
create_input(win_a, win_b) stacks the scenes time-major, B04, B03, B02,
B08 per scene) and that ftw_tools.inference.inference.run passes to the
model unchanged. The window centres are
FTW's summer-crop start and end of season over the study-area bounding box
(ftw_tools.utils.get_harvest_integer_from_bbox and
harvest_to_datetime), with the end of season placed in year + 1 when
it falls before the start (southern-hemisphere seasons), as
ftw_tools.download.download_img.scene_selection does. Each window is a
median composite over centre +/- window_days (engine parameter, default
30) built by the source's composite builder with date_range set, so each
window has its own cache entry. engine_params["window_dates"] (two
"YYYY-MM-DD" centres) replaces the crop calendar. If a window has no
imagery (the composite builder raises
:class:~agribound.composites.base.NoDataError) the run fails with a
:class:RuntimeError (the :class:NoDataError chained as its cause),
unless engine_params["allow_annual_fallback"] is True, in which case the
annual composite is used for that window (logged as a WARNING and recorded in
engine_meta); other builder errors (invalid configuration,
authentication, quota, network) always propagate. Each window's record in
engine_meta["windows"] also holds its composite's image count
(n_images), valid-pixel fraction (valid_fraction) and cloud mask
(cloud_mask), read from the composite's tags. A short window has few
images, and the default Sentinel-2 mask (SCL classes 3, 8, 9 and 10) can
leave haze or thin cloud in the median, which the valid fraction does not
reveal (a Namoi test window B of 11 images had haze over part of the study
area with valid_fraction 1.0). Look at the window composites
(engine_meta["windows"][...]["raster"]) or try
s2_cloud_mask="cloud_score_plus".
:meth:FTWEngine.stage_inputs builds these inputs without running inference
(used by :mod:agribound.hpc.tiles to stage them on a node with network
access). For source="local" a
two-window model needs either stacked_windows=True with a raster whose
bands 1-4 and 5-8 are the two windows (R, G, B, NIR each), or
allow_annual_fallback=True (the single raster is used for both windows).
Single-window models use the input raster.
Radiometry
ftw-tools' default preprocessing divides the input by 3000, i.e. it expects
Sentinel-2 L2A surface reflectance x 10000. Sentinel-2, Landsat and HLS
composites are already on that scale after the 1.0 harmonisation and are used
unchanged (Landsat and HLS are nevertheless outside the Sentinel-2 training
distribution: a WARNING is logged and engine_meta["out_of_distribution_source"]
is True); other value scales are converted with
:func:agribound.io.raster.to_s2_dn. local rasters need
engine_params["value_scale"]. NaN, infinite and declared nodata values
are replaced with 0 before the input raster is written
(:func:write_ftw_input).
Polygonisation
polygonize applies simplify and the morphology options in the units of
the prediction raster's CRS, and computes areas (min_size) in those units
when they are metres. A prediction raster in a geographic CRS, a CRS whose
linear unit is not the metre (e.g. US survey feet) or a Mercator/Web Mercator
CRS is therefore reprojected (nearest neighbour) to the UTM zone of the
study-area centre first (ftw-baselines issue #271;
:func:metric_reprojection_reason), so simplify and min_size are in
metres and m² of UTM or of the raster's own metric projection (other metric
projections, e.g. Albers, keep their small scale distortion).
polygonize processes the mask in windows of polygonization_stride
pixels (default 2048); fields crossing a window edge are split there unless
merge_adjacent is set. close_interiors (default True) fills all holes;
on ftw-tools builds whose polygonize cannot close the interiors of a
MultiPolygon (2.0.0b5), combining it with erode_dilate or
dilate_erode raises :class:ValueError before any input is built.
Engine parameters (all optional)
model, checkpoint_path, window_days (30), window_dates,
allow_annual_fallback (False), stacked_windows (False),
value_scale (local rasters), resize_factor (2), patch_size
(default: :func:select_patch_size), batch_size (2), padding
(ftw-tools default),
softmax_threshold (polygonise field probability >= threshold instead of
the arg-max class; implies save_scores), save_scores (only together
with softmax_threshold), simplify (metres, 0; the pipeline simplifies
later with config.simplify_tolerance), close_interiors (True),
merge_adjacent (None), polygonization_stride (2048), max_size
(m², None), erode_dilate, dilate_erode, erode_dilate_raster,
dilate_erode_raster (0) and thin_boundaries (False).
gdf.attrs["engine_meta"] records the backend ("ftw-tools"), the
ftw-tools version, the model key, the checkpoint URL, the path and SHA-256 of
the checkpoint file (for registry models, the copy ftw-tools caches under
torch.hub.get_dir()/checkpoints), the window dates and how they were
chosen, and the input units.
References
Kerner, H., et al. (2025). Fields of The World: A Machine Learning Benchmark Dataset for Global Agricultural Field Boundary Segmentation. Proceedings of the AAAI Conference on Artificial Intelligence 39(27), 28151-28159. doi:10.1609/aaai.v39i27.35034.
Muhawenayo, G., et al. (2026). PRUE: A Practical Recipe for Field Boundary Segmentation at Scale. arXiv:2603.27101.
FTWEngine ¶
Bases: DelineationEngine
Field boundary delineation with FTW semantic segmentation (see the module docstring).
Source code in agribound/engines/ftw.py
785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 | |
delineate ¶
Run FTW inference and polygonisation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
raster_path
|
str
|
Annual composite (or local raster) for the run. |
required |
config
|
AgriboundConfig
|
Pipeline configuration ( |
required |
Returns:
| Type | Description |
|---|---|
GeoDataFrame
|
Field polygons (in a projected CRS) with
|
Source code in agribound/engines/ftw.py
792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 | |
stage_inputs
staticmethod
¶
Build (or reuse from the cache) the input rasters :meth:delineate reads.
Runs the input stage of :meth:delineate for config and
raster_path without running inference: the model is resolved from
engine_params with :func:resolve_ftw_model, as in
:meth:delineate (ftw-tools' model registry for registry models, the
checkpoint's channel count for checkpoint_path), and for a
two-window model on an Earth Engine
source both seasonal window composites are built by the source's
composite builder with the same window centres, window_days and
cache keys, so a later :meth:delineate with the same configuration
and cache directory finds them without network access (the crop
calendar comes from ftw-tools' cache; see :meth:prefetch).
Single-window models and local sources build nothing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
AgriboundConfig
|
Pipeline configuration. |
required |
raster_path
|
str
|
Annual composite (or local raster) of the run. |
required |
Returns:
| Type | Description |
|---|---|
dict
|
|
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If a window has no imagery and |
ValueError
|
For invalid window parameters or band mappings. |
Source code in agribound/engines/ftw.py
prefetch
classmethod
¶
Download the FTW checkpoint (and the crop calendar for two-window models).
Registry checkpoints are saved where ftw-tools' run() looks for
them (torch.hub.get_dir()/checkpoints/<model>.ckpt); the crop
calendar goes to $FTW_CACHE_DIR/crop_calendar (default
~/.cache/ftw-tools). Set TORCH_HOME and FTW_CACHE_DIR to
shared storage on HPC systems.
Returns:
| Type | Description |
|---|---|
list[str]
|
Local paths of the checkpoint and crop-calendar files. |
Source code in agribound/engines/ftw.py
list_ftw_models ¶
List the models in the installed ftw-tools MODEL_REGISTRY.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
include_legacy
|
bool
|
Include models marked legacy (FTW v1/v2 checkpoints; default False). |
False
|
Returns:
| Type | Description |
|---|---|
dict[str, dict]
|
Model name -> |
Raises:
| Type | Description |
|---|---|
ImportError
|
If ftw-tools is not installed. |
Examples:
>>> from agribound.engines.ftw import list_ftw_models
>>> for name, info in list_ftw_models().items():
... print(f"{name}: {info['title']}")
Source code in agribound/engines/ftw.py
GeoAI¶
geoai_field ¶
GeoAI Mask R-CNN field instance segmentation (geoai-py).
Uses geoai's instance-segmentation workflow (Wu, 2026, JOSS 11(118):9605):
geoai.train.instance_segmentation runs a torchvision Mask R-CNN
ResNet50-FPN (2 classes: background, field) with a sliding window and
class-aware NMS and writes an instance-id raster; each instance is then
vectorised with geoai.utils.raster.raster_to_vector.
No field-boundary weights are published for geoai: as of 2026-09 the Hugging
Face repository giswqs/geoai holds building, car, ship, solar-panel,
parking-spot, water and wetland models and DINOv3 backbone weights; the
field_boundary_detector.pth that geoai.AgricultureFieldDelineator
names by default is not among them, and geoai's default detector weights
(building_footprints_usa.pth) detect buildings. The engine therefore
needs a checkpoint: from fine_tune=True
with reference boundaries (agribound.engines.finetune._geoai) or given as
engine_params["checkpoint_path"] -- a Mask R-CNN ResNet50-FPN state dict
with 2 classes and 3 input channels, as written by geoai's
train_MaskRCNN_model/agribound fine-tuning. It never falls back to other
weights.
Input: canonical R, G, B bands with a scene-level 1-99 percentile stretch to
uint8 (:func:agribound.engines.finetune._data.write_rgb_input), the same
radiometry as the fine-tuning chips; geoai divides by 255. For
source="local" without config.bands bands 1, 2, 3 are read as R, G, B.
Scale: torchvision's Mask R-CNN resizes every input image so that its
shorter side is 800 px (GeneralizedRCNNTransform, min_size=800) at
training and at inference. The apparent size of a field therefore depends on
the image size: a 256 px training chip is enlarged 3.125 times, a 512 px
inference window 1.5625 times. The inference window defaults to the training
chip size recorded next to the checkpoint so that fields appear at the scale
the model was trained on (:func:plan_geoai_windows).
GeoAIEngine ¶
Bases: DelineationEngine
Field delineation with a fine-tuned geoai Mask R-CNN (see module docstring).
Source code in agribound/engines/geoai_field.py
421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 | |
delineate ¶
Run geoai instance segmentation on a composite.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
raster_path
|
str
|
Composite GeoTIFF. |
required |
config
|
AgriboundConfig
|
Pipeline configuration.
|
required |
Returns:
| Type | Description |
|---|---|
GeoDataFrame
|
One polygon per detected field with |
Notes
geoai does not expose two limits of torchvision's Mask R-CNN
(:func:maskrcnn_limits, recorded in engine_meta): at most 100
detections are kept per window (box_detections_per_img), so
where more fields fit in one window (small fields at 10-30 m) the
rest are lost, and detections scoring below 0.05
(box_score_thresh) are discarded, so a confidence_threshold
below 0.05 has the same effect as 0.05 (logged at WARNING). For
dense small fields, fine-tune with a smaller
engine_params["chip_size"]; inference then uses windows of the
same size.
Source code in agribound/engines/geoai_field.py
428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 | |
prefetch
classmethod
¶
Download the weights GeoAI inference and fine-tuning need offline.
- torchvision's COCO Mask R-CNN ResNet50-FPN weights
(
MaskRCNN_ResNet50_FPN_Weights.DEFAULT): geoai builds its model with them before loading a checkpoint, and fine-tuning starts from them. They go to$TORCH_HOME/hub/checkpoints. - The Hugging Face checkpoint, when
engine_params["repo_id"]is set.
Returns:
| Type | Description |
|---|---|
list[str]
|
Local paths. |
Source code in agribound/engines/geoai_field.py
merge_window_seams ¶
merge_window_seams(instance_path: str, output_path: str, window: int, overlap: int, min_seam_px: int = 16, min_seam_fraction: float = 0.5, max_gap_px: int = 2) -> dict[str, Any]
Join instances that one field split into at geoai's window edges.
geoai paints every detection's full mask into one instance raster and keeps the partial detections of a field from overlapping windows when their boxes overlap by less than the NMS threshold, so a field larger than the overlap is split along a window edge (an axis-aligned line at a window start or end), sometimes with a thin gap of background where neither partial mask reaches the edge. For every interior window edge the nearest instances on its two sides are compared row by row (at most max_gap_px background pixels between them): two different instances that meet across the edge along at least min_seam_px pixels, and along at least min_seam_fraction of the shorter of their two runs on that edge, are joined (union-find, so a field cut by several edges becomes one instance). Gaps of at most max_gap_px pixels across an edge between two parts of one (joined) instance are then filled, so no slit is left. Instances that meet anywhere else are left alone.
Writes the relabelled raster to output_path and returns counts for engine_meta.
Source code in agribound/engines/geoai_field.py
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 | |
window_edges ¶
Pixel offsets of the interior window edges geoai's sliding window uses on one axis.
geoai 0.43.1 places windows at min(k * (window - overlap), size - window) (at least
0) for k = 0 .. ceil((size - overlap) / (window - overlap)); each window spans
[start, start + window). The raster borders (0 and size) are left out.
Source code in agribound/engines/geoai_field.py
DINOv3¶
dinov3 ¶
DINOv3 semantic segmentation engine (geoai-py).
Runs geoai's DINOv3Segmenter -- a DINOv3 ViT backbone (Siméoni et al.,
2025, arXiv:2508.10104) with a DPT decoder -- trained by agribound on
reference boundaries (fine_tune=True, see
agribound.engines.finetune._dinov3) into background / field interior /
field boundary classes. There are no published field-boundary weights, so a
fine-tuned Lightning .ckpt is required. Each field interior region is
grown back over the predicted boundary class by the boundary width used in
training (:func:agribound.engines.finetune._data.interior_polygons), so
neighbouring fields do not overlap.
Input
Canonical R, G, B bands with a scene-level 1-99 percentile stretch to uint8
(:func:agribound.engines.finetune._data.write_rgb_input), stored as
float32 uint8 / 255. For source="local" without config.bands
bands 1, 2, 3 are read as R, G, B. geoai divides a window by 255 only when
its maximum exceeds 1 and applies no mean/std normalisation, so the model
sees exactly these [0, 1] values at training and inference. The SAT-493M
backbone was pre-trained with the normalisation mean (0.430, 0.411, 0.296)
and standard deviation (0.213, 0.156, 0.143) (facebookresearch/dinov3
README), so its pre-trained features receive inputs that are not normalised
as in pre-training; this matters most when the backbone is frozen
(use_lora or freeze_backbone). Normalising the chips beforehand is
not a workaround: geoai divides any chip or window whose maximum exceeds 1
by 255.
Sliding window
geoai's dinov3_segment_geotiff zero-pads every window that extends past
the raster to the full window size, and its last window along each axis
starts at min(i * stride, size - 1), so it can extend past the raster.
The ViT attends over the whole window, so zero padding changes the features
of the real pixels. Agribound therefore (:func:plan_dinov3_windows) uses
the training chip size as the window by default, caps the window at the
larger raster side (rounded up to the 16 px patch size), and extends the
RGB input at the bottom and right by mirror reflection so that every window
lies inside it; the prediction is then cropped back to the raster grid.
Weights and offline use
geoai 0.43.1 builds the backbone with torch.hub.load from
facebookresearch/dinov3 (GitHub, or the local clone named by the
DINOV3_LOCATION environment variable) and then loads the SAT-493M ViT-L/16
weights giswqs/geoai / dinov3_vitl16_sat493m.pth from Hugging Face
unless weights_path is given. This also happens when a fine-tuned
checkpoint is loaded for inference (with the weights_path recorded in the
checkpoint); the checkpoint's weights then replace them. SAT-493M weights
exist only for ViT-L/16 and ViT-7B/16, so fine-tuning other backbone sizes
needs engine_params["weights_path"]. :meth:DINOv3Engine.prefetch
downloads the hub repository and the weights for nodes without internet
access.
DINOv3Engine ¶
Bases: DelineationEngine
Field delineation with a fine-tuned DINOv3 + DPT model (see module docstring).
Source code in agribound/engines/dinov3.py
274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 | |
delineate ¶
Run DINOv3 segmentation on a composite.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
raster_path
|
str
|
Composite GeoTIFF. |
required |
config
|
AgriboundConfig
|
Pipeline configuration.
|
required |
Returns:
| Type | Description |
|---|---|
GeoDataFrame
|
Field polygons with |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
Without a checkpoint, or if geoai writes no output. |
ValueError
|
If the checkpoint is not a Lightning |
Source code in agribound/engines/dinov3.py
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 | |
prefetch
classmethod
¶
Download what geoai needs to build the DINOv3 backbone offline.
- The
facebookresearch/dinov3torch.hub repository (skipped whenDINOV3_LOCATIONpoints to a local clone). On nodes without internet access setDINOV3_LOCATIONto the returned directory. - The SAT-493M ViT-L/16 weights from Hugging Face (skipped when
engine_params["weights_path"]is given); setHF_HUB_OFFLINE=1on offline nodes.
Returns:
| Type | Description |
|---|---|
list[str]
|
Local paths (hub repository directory, weights file). |
Source code in agribound/engines/dinov3.py
Prithvi-EO-2.0¶
prithvi ¶
Prithvi-EO-2.0 engine (terratorch).
Prithvi-EO-2.0 (Szwarcman et al., 2026, IEEE TGRS, doi:10.1109/TGRS.2025.3642610)
is a ViT masked-autoencoder pre-trained on HLS Blue, Green, Red, narrow NIR,
SWIR 1 and SWIR 2 surface reflectance. Agribound builds the encoder from the
terratorch backbone registry and runs it on single-date composites
(num_frames=1). Three modes (engine_params["mode"]):
"embed" (label-free)
Patch-token features of one encoder layer (the last, normalised layer by
default) from non-overlapping tiles (tiles that extend past the raster
are filled by mirror reflection of the raster), interpolated bilinearly
between patch-token centres to pixel resolution and clustered with
K-means (fitted on a seeded sample of at most 50 000 pixels); 4-connected
regions of one cluster become polygons. Clusters are land-cover
segments, not field instances.
"segment"
A Prithvi + UPerNet segmentation model fine-tuned by agribound
(fine_tune=True, see agribound.engines.finetune._prithvi) or any
terratorch SemanticSegmentationTask checkpoint trained on the same
six bands and normalisation with class 1 = field interior and class 2 =
field boundary, run with terratorch's tiled_inference on the whole
raster (held in memory with
its class logits). Each field interior region (class 1) is grown back
over the predicted boundary class (2) by the boundary width used in
training, so neighbouring fields do not overlap.
"pca"
Baseline without the ViT: K-means on the PCA of per-band z-scores of R,
G, B, NIR.
The default mode is "segment" when engine_params["checkpoint_path"]
is set (as after fine-tuning) and "embed" otherwise.
Inputs are the six bands of :func:agribound.engines.finetune._data.prithvi_band_names
in surface reflectance x 10000 (the scale of agribound's Sentinel-2, Landsat
and HLS composites), normalised with the Prithvi-EO-2.0 means and standard
deviations (:data:PRITHVI_MEAN, :data:PRITHVI_STD); no other scaling is
applied.
Invalid pixels are set to the band means (0 after normalisation) and to
label 0 in the output. The embed and segment modes need terratorch
(pip install agribound[prithvi] or environment-gfm.yml).
PrithviEngine ¶
Bases: DelineationEngine
Field delineation with Prithvi-EO-2.0 (see the module docstring).
Source code in agribound/engines/prithvi.py
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 | |
delineate ¶
Run Prithvi-based field delineation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
raster_path
|
str
|
Composite GeoTIFF. |
required |
config
|
AgriboundConfig
|
Pipeline configuration.
|
required |
Returns:
| Type | Description |
|---|---|
GeoDataFrame
|
Polygons with |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If |
ValueError
|
For unknown modes, models or non-reflectance inputs. |
Source code in agribound/engines/prithvi.py
306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 | |
prefetch
classmethod
¶
Download the Prithvi-EO-2.0 weights of engine_params["model_name"].
The pre-trained weights are needed by embed mode (unless
engine_params["pretrained"] is False) and by fine-tuning
(config.fine_tune, unless backbone_pretrained is False).
segment inference loads every weight from its checkpoint and
pca mode uses no model, so nothing is downloaded for them. Files
go to the Hugging Face cache (HF_HOME); set HF_HUB_OFFLINE=1
on nodes without internet access afterwards.
Returns:
| Type | Description |
|---|---|
list[str]
|
Local path of the weights file when needed, plus the checkpoint
if |
Source code in agribound/engines/prithvi.py
Embedding clustering¶
embedding ¶
Embedding-based clustering engine.
Clusters pre-computed per-pixel embeddings (Google Satellite Embedding V1, 64-D, or TESSERA, 128-D) with scikit-learn and polygonizes the cluster map. No labels, model weights or GPU are needed. Clusters are land-cover segments, not field instances: every connected region of every cluster becomes a polygon, and non-cropland segments are only removed by the downstream area and LULC filters.
Clustering is memory-bounded: the raster is read in row blocks of at most
engine_params["max_block_mb"] MiB, a seeded uniform random sample of
valid pixels is drawn in one pass, the dimensionality reduction and the
clusterer are fitted on that sample, and a second pass predicts the label of
every valid pixel block by block into an int32 label raster. Polygonization
(:func:agribound.postprocess.polygonize.polygonize_mask) then reads that
label raster at once (4 bytes per pixel).
EmbeddingEngine ¶
Bases: DelineationEngine
Field delineation by unsupervised clustering of pixel embeddings.
Engine parameters (config.engine_params)
use_pca : bool
Reduce the embeddings with PCA before clustering (default True;
only when the raster has more bands than pca_components).
pca_components : int
PCA dimensions (default 16).
matryoshka_depth : int or None
TESSERA v2 only (source="tessera-embedding" with
tessera_version="v2"): cluster the first depth dimensions,
a Matryoshka prefix, instead of PCA. One of :data:MATRYOSHKA_DEPTHS.
Any other source or version raises ValueError.
n_clusters : int or "auto"
Number of clusters, or "auto" (default): the candidate in
k_candidates with the highest silhouette score of a
KMeans(n_init=10) fit on the first silhouette_sample_size
sampled pixels.
clustering_method : str
"kmeans" (default) or "spectral". "kmeans" fits
KMeans(n_init=10) on the cluster sample whatever the raster size
and keeps the lowest-inertia of the ten complete restarts (0.1.x and
1.0.0 used MiniBatchKMeans(batch_size=10000, n_init=3) above
100 000 valid pixels, which ends in a higher-inertia solution for
many samples, and KMeans(n_init=5) otherwise). "spectral" is
SpectralClustering(affinity="nearest_neighbors") on the sample,
extended to all pixels with NearestCentroid (slow).
k_candidates : list[int]
Candidates for n_clusters="auto" (default 5, 10, 15, 20, 30, 50).
pca_sample_size, cluster_sample_size, silhouette_sample_size : int
Sizes of the nested random samples used to fit PCA, the clusterer and
the silhouette selection (defaults 100 000, 50 000, 5 000).
max_block_mb : float
Maximum size of one row block read from the raster (default 256 MiB).
Peak memory is a few times this (a block, the copy of its valid
pixels and per-band masks) plus the fitting sample; GDAL's block
cache is limited to max(64, max_block_mb) MiB during the reads
unless GDAL_CACHEMAX is set in the environment.
A pixel is valid when all bands used (every band, or the Matryoshka
prefix) are finite, not all zero and, when the raster declares a finite
nodata value, not all equal to it. All
random choices (pixel sample, PCA solver, k-means initialisation) are
seeded from config.seed. scikit-learn computes the k-means sums in
parallel, so a different number of OpenMP threads can move a small
fraction of pixels to another cluster. The cluster raster (int32;
0 = invalid, 1..k = cluster) is cached with :func:agribound._cache.cache_path,
keyed by the study area, source, year, TESSERA version, every parameter
above except max_block_mb (the result does not depend on the block
size), the seed and the input raster's path, size and modification time.
When config.sam_refine is True (which also absorbs the legacy
engine_params["sam_refine"]), the polygons are refined with
:func:agribound.engines.samgeo_engine.refine_boundaries on this raster.
An embedding raster has no RGB bands, so this requires
engine_params["sam_rgb_bands"] (three 1-based dimensions used as a
pseudo-RGB image); without it the engine raises before clustering.
Source code in agribound/engines/embedding.py
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 | |
resolve_params
staticmethod
¶
Return the validated engine parameters (defaults filled in).
Raises:
| Type | Description |
|---|---|
ValueError
|
For invalid values. |
Source code in agribound/engines/embedding.py
cluster_cache_path
staticmethod
¶
cluster_cache_path(raster_path: str, config: AgriboundConfig, params: dict[str, Any] | None = None) -> Path
Return the cache path of the cluster raster for raster_path and config.
The key covers :func:agribound._cache.cache_key's fields plus the
seed, the raster's resolved path, size and modification time, and all
engine parameters except max_block_mb.
Source code in agribound/engines/embedding.py
prefetch
classmethod
¶
Download the SAM weights when config.sam_refine is set (nothing else is remote).
Source code in agribound/engines/embedding.py
delineate ¶
Cluster the embedding raster and polygonize the clusters.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
raster_path
|
str
|
Embedding GeoTIFF (float32, one band per embedding dimension). |
required |
config
|
AgriboundConfig
|
Pipeline configuration. |
required |
Returns:
| Type | Description |
|---|---|
GeoDataFrame
|
Polygons with |
Raises:
| Type | Description |
|---|---|
ValueError
|
For an unsupported source, invalid parameters, a raster with too
few valid pixels, or SAM refinement without |
Source code in agribound/engines/embedding.py
Ensemble¶
ensemble ¶
Ensemble engine: several engines (or one engine with different models) on the same raster, combined by intersection, union or pixel vote.
Merge strategies
"intersection" (default)
Successive :func:geopandas.overlay how="intersection" of the
members' polygons: the output pieces are the areas every member covers,
one piece per combination of overlapping polygons. Pieces are not
re-merged, so where members' boundaries disagree small sliver pieces can
appear next to the main piece of a field; the pipeline's area filter
removes those below min_field_area_m2.
"union"
All members' polygons are pooled and duplicates are fused with
:func:agribound.postprocess.merge.merge_polygons (IoU >=
union_iou_threshold or containment >= union_containment_threshold;
defaults 0.3 and 0.8). Fields that only touch stay separate.
"vote"
Each member's polygons are rasterised (pixel centres) onto the input
raster's grid; a pixel is kept when at least min_votes members cover
it, and the kept pixels are polygonised (4-connectivity). As in
agribound 0.1.x, members that returned no polygons are left out of the
vote (logged at WARNING and listed in vote_stats["empty_members"]),
and by default min_votes = max(min(2, n), ceil(vote_threshold * n))
for the n remaining members: at least vote_threshold of them,
and at least two whenever there are two or more, so that one member's
false positives never pass on their own. engine_params["min_votes"]
sets it directly (1..n). Members skipped after an error
(on_member_error="skip") do not count either. Adjacent fields that
are both kept become one polygon wherever they touch on the pixel grid,
because the vote raster is binary (field / not field).
With a single member (configured, or the only one left with
on_member_error="skip") that member's frame is returned as it is (all
its columns), plus an engine_count column.
EnsembleEngine ¶
Bases: DelineationEngine
Multi-engine or multi-model ensemble (see the module docstring for the strategies).
Engine parameters (config.engine_params)
engines : list[str | dict]
Members; each is an engine name or a dict with "engine", optional
"engine_params" and optional "label" (default: the member's
engine_params["model"] or the engine name; a repeated label gets
an _<index> suffix, counting up from the member's position until
it is unique). Default ["delineate-anything", "ftw"]. Every
member, including the defaults, must support config.source.
merge_strategy : str
"intersection" (default), "union" or "vote".
vote_threshold : float
Vote strategy: fraction in [0, 1] of the members with polygons that
must agree (default 0.5); at least two members must agree whenever
two or more have polygons.
min_votes : int or None
Vote strategy: explicit minimum number of agreeing members (1..n,
where n counts the members with polygons); overrides
vote_threshold.
vote_resolution : float or None
Vote grid cell size in CRS units. None (default) uses the input
raster's own grid.
union_iou_threshold, union_containment_threshold : float
Union strategy duplicate criteria (defaults 0.3, 0.8).
on_member_error : str
"raise" (default): a failing member aborts the run. "skip":
continue without it; the failure is logged as a warning and listed
in engine_meta["failed_members"].
isolate_member_caches : bool
Give every member its own cache directory
<working dir>/ensemble/<slug> (default True), so members with
different models never reuse each other's intermediates. The slug is
the label with characters other than letters, digits, ., _
and - replaced by _; a slug that repeats, ignoring case, gets
an _<index> suffix. Members that build their own window composites
(FTW) then download them into their own directory. False shares
the ensemble's cache directory.
Members receive only the engine_params of their own spec; they do
not inherit the ensemble-level engine_params. Ensemble-level keys
other than the ones above and the pipeline/SAM keys in
:data:PIPELINE_KEYS raise ValueError (put them in a member spec).
Every member runs with sam_refine=False (the pipeline refines the
ensemble output instead) and is seeded with config.seed right before
it runs, so its result does not depend on the member order.
Output columns: engine_count (intersection and union: number of
members that ran without an error; vote: number of those with polygons)
plus, per strategy,
ensemble:members (intersection: all member labels; union: labels of
the members whose polygons were fused, comma-separated),
ensemble:n_members (union), or vote_count (maximum number of
agreeing members inside the polygon; in 0.1.x this column held the
constant min_votes), vote_count_mean and min_votes (vote).
Member attribute columns are not carried over, except with a single
member, whose frame is returned as it is plus engine_count.
attrs["engine_meta"] lists every member's label, engine, parameters,
polygon count, cache directory and own engine_meta.
Source code in agribound/engines/ensemble.py
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 | |
member_specs
staticmethod
¶
Return the normalised member specs.
Each spec is {"label", "engine", "engine_params", "cache_slug"};
labels and cache slugs are unique.
Raises:
| Type | Description |
|---|---|
ValueError
|
For an empty or malformed member list, an unknown engine, a nested
ensemble, or a member that does not support |
Source code in agribound/engines/ensemble.py
resolve_params
staticmethod
¶
Return the validated ensemble-level parameters.
Raises:
| Type | Description |
|---|---|
ValueError
|
For unknown ensemble-level keys or invalid values. |
Source code in agribound/engines/ensemble.py
member_config
staticmethod
¶
member_config(config: AgriboundConfig, spec: dict[str, Any], isolate_cache: bool = True) -> AgriboundConfig
Return the validated configuration a member runs with.
Same fields as config except engine, engine_params (the
member's own), sam_refine=False and, with isolate_cache,
cache_dir=<working dir>/ensemble/<spec["cache_slug"]> (the slug
of the label when the spec has no cache_slug).
Source code in agribound/engines/ensemble.py
prefetch
classmethod
¶
Prefetch every member's weights (duplicates removed, order kept).
Source code in agribound/engines/ensemble.py
stage_inputs
classmethod
¶
Build the inputs that members build themselves, without running them.
For every member whose engine class has a stage_inputs method
(currently FTW: :meth:agribound.engines.ftw.FTWEngine.stage_inputs,
the two seasonal window composites), calls it with the member's own
configuration (:meth:member_config, including its isolated cache
directory) and raster_path, so a later :meth:delineate with the
same configuration finds the inputs in the member caches without
network access. Used by :mod:agribound.hpc.tiles to stage ensemble
tiles.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
AgriboundConfig
|
Ensemble configuration. |
required |
raster_path
|
str
|
Input raster shared by the members. |
required |
Returns:
| Type | Description |
|---|---|
dict
|
|
Raises:
| Type | Description |
|---|---|
Exception
|
A member's staging error when |
Source code in agribound/engines/ensemble.py
delineate ¶
Run every member on raster_path and merge the results.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
raster_path
|
str
|
Input GeoTIFF shared by all members. |
required |
config
|
AgriboundConfig
|
Pipeline configuration ( |
required |
Returns:
| Type | Description |
|---|---|
GeoDataFrame
|
Merged polygons in the raster CRS, with |
Raises:
| Type | Description |
|---|---|
ValueError
|
For invalid ensemble parameters or members. |
RuntimeError
|
If every member failed ( |
Source code in agribound/engines/ensemble.py
374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 | |
SAM refinement¶
SAM 3 backends are untested
sam_backend="sam3" and "sam3-hf" have not been run end to end with
agribound 1.0.1 (the facebook/sam3 weights are gated); a WARNING is
logged when one is loaded. See
SAM refinement.
samgeo_engine ¶
Box-prompted SAM refinement of field boundaries.
This is a post-processing stage, not a delineation engine: every polygon's
bounding box is given to a Segment Anything model as a single-instance box
prompt, and the polygon is replaced by the mask SAM returns, unless that mask
covers too little of it (step 7). The pipeline runs it after delineation when
config.sam_refine is True (the embedding engine calls it itself).
Algorithm (:func:refine_boundaries)
- Polygons are reprojected to the raster CRS. The raster must not be rotated or sheared.
- Gating. Missing or empty geometries are skipped. So is every polygon
whose bounding box extends more than half a pixel
(:data:
EDGE_TOLERANCE_PX) beyond the raster (counted inn_skipped_outside): SAM would see only part of such a field, and a mask of the visible part would truncate it. Polygons delineated from the raster itself normally lie inside it. Of the remaining polygons, one is refined only if both sides of its padded bounding box are at leastconfig.sam_min_crop_pxpixels, where the padded side isfloor(side_px * (1 + 2 * config.sam_crop_padding))andside_pxis the bounding-box width (height) divided by the pixel width (height). :func:crop_window_pxand :func:is_refinableimplement exactly this test (passraster_boundsto :func:is_refinableto include the inside-the-raster test). Skipped polygons keep their geometry. Boxes within the half-pixel tolerance, and padded boxes, are clipped to the raster. - Image. The canonical R, G, B bands of
config.source(withconfig.bandstaking precedence, orengine_params["sam_rgb_bands"]) are converted to uint8 with one scene-wide percentile stretch: the 1st and 99th percentiles of each band are computed by :func:agribound.io.raster.percentile_stretch_uint8(valid, finite, positive pixels) on a nearest-neighbour decimated read of at most 4096 pixels per side, and every window is stretched with those same bounds. uint8 rasters are used as-is. For embedding rasters (signed values) the percentiles are taken over all finite values. - Windows. Polygons whose padded box (clipped to the raster) fits in
window_px - window_px // 2pixels on both axes are assigned to a grid ofwindow_pxxwindow_pxwindows with a stride ofwindow_px // 2(windows at the raster edge are shifted inwards to keep their full size); each window is encoded once and all its boxes are decoded in batches ofbatch_size. Larger polygons get their own square window, with the side of their padded box's longer axis, centred on the box. A field window whose side exceedsmax_window_px = max(2 * window_px, 2048)is read with nearest-neighbour decimation so that its longer side ismax_window_px, which bounds memory (SAM downsamples it to its input size in any case). SAM resizes every window to a fixed square input without keeping the aspect ratio (1024 x 1024 px for SAM 2/2.1, 1008 x 1008 px for SAM 3), so windows that are not square (the raster is smaller than the window on one axis, or a field window is clipped by the raster) are padded with black (0) pixels on the right and bottom to a square first; box coordinates are unaffected. A full grid window is encoded at about its native scale. This differs from agribound 0.1.x, which encoded each field's padded crop on its own, so that SAM upsampled a 64 px crop 16-fold; a smallerengine_params["sam_window_px"](at least2 * sam_min_crop_px) restores part of that zoom at the cost of more encoder passes. - Masks. One mask per box (
multimask_output=False). As in 0.1.x, where the image was the field's padded crop, only the part of the mask inside the field's padded box (rounded outwards to whole pixels and clipped to the raster) is used. It is vectorised with :func:rasterio.features.shapes, the largest polygon is kept (holes included), repaired if invalid, and reprojected to the input CRS. Boxes are passed as continuous pixel coordinates (not truncated). Steps 6 and 7 decide whether the mask replaces the input polygon. - Overlaps (
engine_params["sam_overlaps"], default"trim"). A mask may grow over a neighbouring polygon, and both would be kept. With"trim"a refined polygon never takes area that another input polygon covered and it did not (:func:trim_refinement_overlaps), and where two refined masks grew over the same new area, the one with the higher SAM score keeps it. The overlap between the refined polygons and the others is therefore never larger than between the input polygons, so SAM adds no overlap to an engine output without overlaps (Delineate-Anything resolves them). The trimmed mask keeps its largest part (step 7 then tests it); a mask with nothing left keeps the input geometry and counts as failed."keep"keeps the masks as SAM drew them (withsam_min_coverage=0, the behaviour before 1.0.0), so outputs may overlap. Trade-off, measured on 2026-09-28 with Delineate-Anything (large_v2) + SAM 2 (sam2-hiera-large, MPS) on the Namoi test area (Sentinel-2, 2023; 16 of 230 polygons refined): the post-processed output had 3.59 ha of overlap with"keep"(3.45 ha between a refined polygon and a neighbour) and 0.20 ha with"trim", against 0.18 ha without SAM. With"keep"one refined mask grew over the neighbours of one of the four reference fields in the area, raising that field's best IoU from 0.636 (no SAM) to 0.773; with"trim"it is 0.676 (the other three fields: unchanged or within 0.02). SAM's growth over an engine boundary can be a correction (a field split in two) or a leak into a real neighbour; the default keeps the engine's boundaries between polygons. - Coverage (
engine_params["sam_min_coverage"], default :data:DEFAULT_MIN_COVERAGE= 0.5). SAM returns one object per box, so when an input polygon holds several fields (an embedding cluster, or fields an engine merged) the mask can follow one of them, and the rest of the polygon's area would be left without a polygon. A mask that covers less thansam_min_coverageof its input polygon,area(mask & input) / area(input)after step 6 (an invalid input is repaired first), is not used: the polygon keeps its input geometry,agribound:sam_refinedis False andagribound:sam_scoreNaN, andn_low_coveragecounts it. With"trim"each mask is tested right after its trim, in step 6's score order, so a rejected mask claims no area from the lower-scoring masks. An input without area is never rejected; 0 turns the test off (the 1.0.0 behaviour). Measured on 2026-09-29 (SAM 2sam2-hiera-large, MPS, outputs after the area filter, smoothing and simplification), no test -> 0.5: example 15's Sentinel-2 refinement of the TESSERA (Google) crop polygons left 8 -> 1 (14 -> 2) of 29 checked centre pivots less than half covered and lost 15.7 -> 7.4 % (24.1 -> 8.1 %) of the input area (48 of 510, 95 of 439 masks rejected); example 13's input (Delineate-Anything, Sentinel-2, 67 masks, each covering >= 92 % of its polygon) did not change. On example 14's DINOv3 NAIP (SPOT) polygons of Lea County the reference fields less than half covered went from 35 (67) without SAM to 80 (105) with SAM and 53 (85) with 0.5, and F1 from 0.604 (0.423) to 0.609 (0.479) and 0.590 (0.445): a rejected mask often matched one of the several fields its polygon held. 0.7 restored more coverage (no pivot missing; 40 (77) fields) at an F1 of 0.595 (0.418).
Backends (config.sam_backend)
"sam2":samgeo.SamGeo2(model_id, automatic=False).predictor(SAM2ImagePredictor); SAM 2.0 checkpointsfacebook/sam2-hiera-*."sam2.1":sam2.sam2_image_predictor.SAM2ImagePredictor.from_pretrainedwithfacebook/sam2.1-hiera-*checkpoints (SamGeo2 accepts only SAM 2.0 ids).apply_postprocessing=Falseas in SamGeo2, so the two differ only in their weights."sam3":samgeo.SamGeo3(backend="meta", enable_inst_interactivity=True)andpredict_inst(box=...)(SAM 3 instance-interactive, i.e. SAM 1/2 style single-object prompts). Needs a CUDA GPU andtriton, which the Meta package imports at import time: Linux is the platform Meta supports; Windows works only through the communitytriton-windowswheel (not verified by agribound; a WARNING is logged); macOS is not supported. Gated weightsfacebook/sam3orfacebook/sam3.1."sam3-hf":transformers.Sam3TrackerModel/Sam3TrackerProcessor(promptable visual segmentation, one mask per box). No triton dependency, so it is the SAM 3 option for platforms without triton (Windows without triton-windows, macOS); it imports on macOS, but agribound has not yet run it end to end on any platform because the weights are gated. CUDA recommended. Gated weightsfacebook/sam3.
Concept-exemplar (PCS) box prompts such as SamGeo3.generate_masks_by_boxes,
which segment all objects similar to the box, are never used.
refine_boundaries ¶
refine_boundaries(gdf: GeoDataFrame, raster_path: str, config: AgriboundConfig, **kwargs: Any) -> gpd.GeoDataFrame
Refine field boundaries with box-prompted SAM (see the module docstring).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
gdf
|
GeoDataFrame
|
Field boundaries from a delineation engine. |
required |
raster_path
|
str
|
GeoTIFF the polygons were delineated from (not rotated or sheared). |
required |
config
|
AgriboundConfig
|
Uses |
required |
**kwargs
|
Any
|
|
{}
|
Returns:
| Type | Description |
|---|---|
GeoDataFrame
|
Copy of gdf (same rows, order, index and columns) with geometry
replaced where refined, a bool column |
Raises:
| Type | Description |
|---|---|
ValueError
|
For a rotated raster, bad band indices, an embedding raster without
|
RuntimeError
|
If SAM raised for every window that had prompts. |
Notes
Masks depend on the compute device: on a Sentinel-2 test crop, SAM 2
(sam2-hiera-tiny) masks computed on Apple MPS overlapped the CPU
masks of the same fields with IoU between 0.59 and 0.97. sam_stats
records the device.
Source code in agribound/engines/samgeo_engine.py
1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 | |
crop_window_px ¶
crop_window_px(bounds: tuple[float, float, float, float], pixel_size: tuple[float, float], padding: float) -> tuple[int, int]
Return the padded bounding-box size in whole pixels, as used for gating.
width_px = floor((maxx - minx) / |pixel_size[0]| * (1 + 2 * padding))
and likewise for the height with pixel_size[1] (a tolerance of 1e-6
pixel absorbs floating-point error).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bounds
|
tuple of float
|
|
required |
pixel_size
|
tuple of float
|
Pixel width and height in CRS units (sign ignored). |
required |
padding
|
float
|
Padding on each side as a fraction of the box size (>= 0). |
required |
Returns:
| Type | Description |
|---|---|
tuple[int, int]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If a pixel size is not positive or padding is negative. |
Source code in agribound/engines/samgeo_engine.py
is_refinable ¶
is_refinable(bounds: tuple[float, float, float, float], pixel_size: tuple[float, float], min_crop_px: int = MIN_CROP_SIZE, padding: float = CROP_PADDING, raster_bounds: tuple[float, float, float, float] | None = None) -> bool
Return True if :func:refine_boundaries would prompt SAM with this box.
A polygon is refined only when both sides of its padded bounding box
(:func:crop_window_px) are at least min_crop_px pixels. With the
defaults (64 px, 15 % padding) the unpadded box must be at least
64 / 1.3 = 49.2 pixels on each side.
:func:refine_boundaries also skips every polygon whose bounding box
extends more than :data:EDGE_TOLERANCE_PX (half a pixel) beyond the
raster. Without raster_bounds this function assumes the polygon lies
inside the raster (normally true for polygons delineated from it); with
raster_bounds it applies that test too and then reproduces
:func:refine_boundaries exactly.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bounds
|
tuple of float
|
|
required |
pixel_size
|
tuple of float
|
Pixel width and height in CRS units. |
required |
min_crop_px
|
int
|
Minimum padded side in pixels ( |
MIN_CROP_SIZE
|
padding
|
float
|
Padding fraction ( |
CROP_PADDING
|
raster_bounds
|
tuple of float or None
|
Raster extent |
None
|
Returns:
| Type | Description |
|---|---|
bool
|
|
Source code in agribound/engines/samgeo_engine.py
prefetch ¶
Download the weights of the configured SAM backend into the Hugging Face cache.
Afterwards refinement can run with HF_HUB_OFFLINE=1. Files:
sam2/sam2.1: the checkpoint named in
sam2.build_sam.HF_MODEL_ID_TO_FILENAMES; sam3: config.json,
sam3.pt (or sam3.1_multiplex.pt) and the text-encoder vocabulary;
sam3-hf: the repository's *.json and *.safetensors files.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
AgriboundConfig
|
Uses |
required |
Returns:
| Type | Description |
|---|---|
list[str]
|
Local paths of the downloaded files (or snapshot directory). |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If a gated repository refuses access. |
Source code in agribound/engines/samgeo_engine.py
Fine-tuning¶
finetune ¶
Fine-tuning on reference field boundaries.
:func:fine_tune adapts a fine-tunable engine to user-supplied reference
polygons and returns the path of the resulting checkpoint. The pipeline then
passes that path to the engine as engine_params["checkpoint_path"].
Engine-specific code lives in private submodules, which the dispatcher imports only when it needs them:
_data: training chips, segmentation masks and the train/validation split (_prepare_training_data(raster_path, config, engine) -> Path)_yolo: Delineate-Anything / Ultralytics YOLO (_finetune_yolo(train_dir, config, model_key) -> str)_geoai: GeoAI Mask R-CNN (_finetune_geoai(train_dir, config) -> str)_dinov3: DINOv3 + DPT head via geoai (_finetune_dinov3(train_dir, config) -> str)_prithvi: Prithvi-EO-2.0 via terratorch (_finetune_prithvi(train_dir, config) -> str)_ftw: not dispatched. FTW models are trained with ftw-baselines.
Which engines can be fine-tuned is read from
:data:agribound.registry.ENGINE_REGISTRY (fine_tunable). Any other engine
raises :class:ValueError with instructions. The engine is never replaced by
a different one.
Caching
Each fine-tuning run gets its own directory from
:func:agribound._cache.cache_path. The key covers everything
:func:agribound._cache.cache_key hashes (study area, source, year/date range,
compositing and export settings) plus the engine, the base-model id, a
fingerprint of the reference file (resolved path, modification time and size),
the number of epochs, the split settings, the seed, config.bands, the
engine_params (excluding checkpoint_path and sam_* keys), the
engine's default chip-size rule (:func:agribound.engines.finetune._data.chip_size_rule)
and, for Delineate-Anything, the version of the training recipe
(:data:agribound.engines.finetune._yolo.RECIPE_VERSION), so a checkpoint
trained with an earlier recipe is not reused. The trainers receive a copy of
the configuration whose
:meth:~agribound.config.AgriboundConfig.get_working_dir is that directory.
Their chips and checkpoints therefore cannot collide with another run's.
finetune_manifest.json in the directory records the checkpoint, and a later
call with the same key returns it without retraining.
fine_tune ¶
Fine-tune the configured engine on reference field boundaries.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
raster_path
|
str
|
Path to the satellite composite GeoTIFF. |
required |
config
|
AgriboundConfig
|
Pipeline configuration with |
required |
Returns:
| Type | Description |
|---|---|
str
|
Absolute path to the fine-tuned model checkpoint (cached or new). |
Raises:
| Type | Description |
|---|---|
ValueError
|
If reference boundaries are not provided, or if |
NotImplementedError
|
If the registry marks the engine as fine-tunable but no trainer is wired up for it here. |
RuntimeError
|
If the trainer does not return an existing checkpoint file. |
Source code in agribound/engines/finetune/__init__.py
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | |