Evaluation¶
Object-level accuracy assessment of field polygons. See Evaluation for the definitions in one place.
evaluate ¶
Object-level accuracy assessment of field-boundary polygons.
Predicted field polygons are compared with reference field polygons. The functions here report, for a prediction layer and a reference layer:
- object detection metrics — precision, recall and F1 of polygon matches at an IoU threshold (one-to-one or legacy many-to-one matching);
- area-weighted metrics — the fraction of reference (predicted) area that belongs to matched fields;
- segmentation errors — over- and under-segmentation of each reference field against the prediction it overlaps most (Persello & Bruzzone, 2010);
- boundary metrics — Hausdorff and symmetric mean boundary distance for matched pairs, boundary precision/recall/F1 within a distance tolerance (buffered-line method) and the fraction of reference fields that are matched with a mean boundary distance within that tolerance;
- the same metrics per stratum (a reference attribute) and per field-size class, with percentile bootstrap confidence intervals obtained by resampling reference fields.
:func:evaluate returns the summary dictionary, :func:evaluate_frame the
per-reference-field table the summary is built from, and
:func:pixels_per_field the number of pixels a field spans at a given ground
sample distance.
References
Clinton, N., Holt, A., Scarborough, J., Yan, L., Gong, P., 2010. Accuracy assessment measures for object-based image segmentation goodness. Photogrammetric Engineering & Remote Sensing 76 (3), 289-299. https://doi.org/10.14358/PERS.76.3.289
Persello, C., Bruzzone, L., 2010. A novel protocol for accuracy assessment in classification of very high resolution images. IEEE Transactions on Geoscience and Remote Sensing 48 (3), 1232-1244. https://doi.org/10.1109/TGRS.2009.2029570
Stehman, S.V., Foody, G.M., 2019. Key issues in rigorous accuracy assessment of land cover products. Remote Sensing of Environment 231, 111199. https://doi.org/10.1016/j.rse.2019.05.018
MATCHING_METHODS
module-attribute
¶
evaluate ¶
evaluate(predicted: GeoDataFrame | GeoSeries, reference: GeoDataFrame | GeoSeries, iou_threshold: float = 0.5, *, matching: str = 'one_to_one', strata: str | Series | Sequence[Any] | ndarray | None = None, size_bins: str | Sequence[float] | ndarray | None = None, bootstrap: int = 0, bootstrap_seed: int = 42, boundary_tolerance_m: float | None = None, equal_area_crs: Any = None, boundary_sample_spacing_m: float | None = BOUNDARY_SAMPLE_SPACING_M) -> dict[str, Any]
Compute object-level accuracy metrics of predicted field polygons.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
predicted
|
GeoDataFrame or GeoSeries
|
Predicted field polygons. Must have a CRS unless it contains no geometries. |
required |
reference
|
GeoDataFrame or GeoSeries
|
Reference field polygons. Must have a CRS unless it contains no geometries (it may differ from the prediction's CRS). |
required |
iou_threshold
|
float
|
Minimum IoU (intersection over union) for a prediction and a reference
field to match, in |
0.5
|
matching
|
('one_to_one', 'many_to_one')
|
|
"one_to_one"
|
strata
|
(str, Series or array - like)
|
Reference stratum of each reference field: the name of a column of
reference, a Series aligned with |
None
|
size_bins
|
"auto" or sequence of float
|
Field-size classes, in hectares (1 ha = 10,000 m²), of the
reference field area in equal_area_crs. Either |
None
|
bootstrap
|
int
|
Number of bootstrap resamples for confidence intervals (0 disables). |
0
|
bootstrap_seed
|
int
|
Seed (a non-negative integer) of the bootstrap random number generator. |
42
|
boundary_tolerance_m
|
float
|
Distance tolerance in metres. When given, boundary precision/recall/F1 and coverage within tolerance are computed. |
None
|
equal_area_crs
|
CRS - like
|
Projected CRS with axes in metres used for areas and IoU (anything
:meth: |
None
|
boundary_sample_spacing_m
|
float or None
|
Maximum spacing in metres between the sample points of each boundary
for the Hausdorff and mean boundary distances. The default,
|
BOUNDARY_SAMPLE_SPACING_M
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
Keys kept from 0.1.x (
Added in 1.0:
With
With With Conventions for empty denominators: the rates (keys ending in
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If an input is not a GeoDataFrame/GeoSeries. |
ValueError
|
If an input that contains geometries has no CRS, or an argument is out of range. |
ImportError
|
If a layer contains invalid geometries and the installed shapely does
not provide |
Notes
Geometry handling. Invalid geometries are repaired with
shapely.make_valid(method="structure", keep_collapsed=False) and only
their polygonal parts are kept; null, empty, non-polygonal and zero-area
geometries are excluded (counted in count_*_dropped). The repair
needs shapely >= 2.1 built with GEOS >= 3.10. With older versions an
ImportError is raised when a repair is needed, rather than switching to
the "linework" method, which repairs some geometries differently. A layer
without a CRS is accepted only when it has no geometries (for example an
empty prediction). Layers that cross the antimeridian are not supported.
Coordinate systems. Areas and IoU are computed in equal_area_crs. Geometries are reprojected vertex by vertex, so each edge becomes a straight chord in the target CRS. Before reprojection to equal_area_crs, edges are split into pieces of at most 50 m in the source CRS. The pieces are collinear there, so the polygons are unchanged in their own CRS. Without this split, a long edge with few vertices would become a single chord, while a coincident edge of the other layer with more vertices follows the curved image of the line, and the two would no longer coincide. For a 2 km × 10 m field that changed the IoU by up to 0.4 % at 34° latitude and 1.5 % at 70°.
With the split, we compared against the planar IoU in the source UTM CRS (inputs in UTM zone 13N). For 100 m and 1 km shifted squares and a notched 2 km × 10 m field, at latitudes from 0.5° to 70°, on and 2.7° off the central meridian, the relative difference was at most 5.4e-6. For 1,500 fields polygonised at 1 m, compared with their own ~20-vertex polygons at 34° N, it was at most 2.5e-7.
Distances (Hausdorff, boundary distances and the tolerance metrics) are computed in the WGS 84 / UTM zone of each polygon's centroid: reference fields and their matched predictions in the reference field's zone, and predicted boundaries in the prediction's zone for boundary precision. Inside a zone the linear scale error is below about 0.1 %. Geometries are not split before reprojection to UTM. From a source CRS in the same zone (for example NAD83 / UTM 13N into WGS 84 / UTM 13N) edges stay straight to within 1e-8 m. A 1 km edge along a parallel that is straight in EPSG:4326 is replaced by a chord up to 1.3 cm from its image at 34° latitude, and 3.4 cm at 60°. The deviation grows with the square of the edge length.
Candidate pairs come from a shapely STRtree query with the
"intersects" predicate; intersection areas are computed with
vectorised shapely operations.
Boundary distances are computed by sampling each boundary (interior
rings included) at points at most Δ apart, keeping every original
vertex, and taking the exact distance from each sample to the other
boundary. Δ is boundary_sample_spacing_m (1 m by default), or,
when that is None, max(BOUNDARY_SAMPLE_SPACING_M, perimeter /
BOUNDARY_MAX_SAMPLES): 1 m, or perimeter / 1000 for boundaries longer
than 1 km. The distance to a boundary is 1-Lipschitz. So, with Δ the
larger spacing of the two boundaries of a pair
(boundary_sample_spacing_m in :func:evaluate_frame), the Hausdorff
distance is underestimated by at most Δ / 2, and the symmetric mean
distance (trapezoidal rule) differs from the exact line-integral mean by
at most Δ / 4.
Tolerance metrics are exact up to floating-point rounding. Boundaries are split into their straight segments. For each pair of nearby segments, one from each layer, the part of the first within the tolerance of the second — its intersection with the second's tolerance region, a rectangle capped by two discs — is computed in closed form, and the union of these parts along each segment is measured. This is the length inside the circular-arc buffers of the other layer's boundaries, with no polygonal approximation of the arcs. Run time grows with the number of boundary segments within the tolerance of each other.
Bootstrap. Each resample draws reference fields with replacement (the
same number as evaluated); with strata, every stratum — and the set of
fields with a missing stratum — is resampled separately at its own size.
A resampled field brings its per-field results and the predictions whose
home it is; predictions overlapping no reference field are kept unchanged
in every resample, so the intervals reflect sampling variability of the
reference fields only. Intervals are percentile intervals at
BOOTSTRAP_CONFIDENCE_LEVEL (95 %) over the resamples in which the
metric is defined; resamples are generated from child seeds of
numpy.random.SeedSequence(bootstrap_seed), so the resamples do not
depend on how they are batched. Within a resample a rate whose
denominator is 0 — for example any metric of a size class none of whose
fields was drawn — is undefined rather than 0, and n_undefined counts
such resamples. Medians have no intervals. The intervals treat the
evaluated reference fields as an independent random sample (a stratified
random sample when strata is given). They do not model spatial
autocorrelation between neighbouring fields, so they are too narrow when
errors are spatially clustered, and they are not design-based estimates
for a probability sample of reference fields (see Stehman & Foody, 2019,
on sampling design and analysis).
Examples:
>>> from agribound.evaluate import evaluate
>>> metrics = evaluate(predicted_gdf, reference_gdf, boundary_tolerance_m=5.0)
>>> print(f"F1: {metrics['f1']:.3f}, area-weighted recall: "
... f"{metrics['area_weighted_recall']:.3f}")
Source code in agribound/evaluate.py
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 | |
evaluate_frame ¶
evaluate_frame(predicted: GeoDataFrame | GeoSeries, reference: GeoDataFrame | GeoSeries, *, iou_threshold: float = 0.5, matching: str = 'one_to_one', strata: str | Series | Sequence[Any] | ndarray | None = None, equal_area_crs: Any = None, boundary_tolerance_m: float | None = None, size_bins: str | Sequence[float] | ndarray | None = None, boundary_sample_spacing_m: float | None = BOUNDARY_SAMPLE_SPACING_M) -> pd.DataFrame
Return the per-reference-field evaluation table.
One row per evaluated reference field (null, empty, non-polygonal and
zero-area reference geometries are excluded), indexed by the reference
index, so the table can be joined back to reference. The arguments and
methods are those of :func:evaluate; the summary returned by
:func:evaluate is computed from the same per-field results.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
predicted
|
GeoDataFrame | GeoSeries
|
See :func: |
required |
reference
|
GeoDataFrame | GeoSeries
|
See :func: |
required |
iou_threshold
|
GeoDataFrame | GeoSeries
|
See :func: |
required |
matching
|
GeoDataFrame | GeoSeries
|
See :func: |
required |
strata
|
GeoDataFrame | GeoSeries
|
See :func: |
required |
equal_area_crs
|
GeoDataFrame | GeoSeries
|
See :func: |
required |
boundary_tolerance_m
|
float
|
See :func: |
None
|
size_bins
|
"auto" or sequence of float
|
See :func: |
None
|
boundary_sample_spacing_m
|
float or None
|
See :func: |
BOUNDARY_SAMPLE_SPACING_M
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Columns:
|
Source code in agribound/evaluate.py
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 | |
pixels_per_field ¶
pixels_per_field(reference: GeoDataFrame | GeoSeries, gsd_m: float, equal_area_crs: Any = None) -> pd.Series
Return the number of pixels each field spans at a ground sample distance.
p = A / gsd_m², with A the field area in m² in equal_area_crs:
the number of gsd_m × gsd_m pixels whose total area equals the field
area (not a count of pixel centres inside the field).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
reference
|
GeoDataFrame or GeoSeries
|
Field polygons. Must have a CRS unless it contains no geometries (then every value is NaN). |
required |
gsd_m
|
float
|
Ground sample distance (pixel size) in metres, > 0. |
required |
equal_area_crs
|
CRS - like
|
Projected CRS for areas (default EPSG:6933). As in :func: |
None
|
Returns:
| Type | Description |
|---|---|
Series
|
Float series named |
Raises:
| Type | Description |
|---|---|
TypeError
|
If reference is not a GeoDataFrame/GeoSeries. |
ValueError
|
If gsd_m is not a positive finite number, equal_area_crs is not a projected CRS in metres, or reference contains geometries but has no CRS. |
ImportError
|
If invalid geometries need repair and the installed shapely does not
provide |