Skip to content

Commit a9e2749

Browse files
authored
Add Mapbox source class probe candidates (#1356)
Add ranked camera/class probe candidates to the Mapbox Outdoors source/crop aggregate report. Part of #949.
1 parent 1d43837 commit a9e2749

3 files changed

Lines changed: 151 additions & 2 deletions

File tree

docs/mapbox-outdoors-comparison-harness.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ python3 validation/mapbox_outdoors_source_crop_overlap.py \
209209
--aggregate-output /tmp/source-crop-overlap-aggregate.md
210210
```
211211

212-
The aggregate Markdown summarizes source-layer coverage sums, top class coverage within each source layer, QGIS style-layer coverage sums, distinct QGIS runtimes, and per-camera rows with class counts and class coverage from the input reports. Use it to choose the next owner-mask or missing-class probe across the camera matrix; it remains bbox attribution, not rendered-pixel ownership or a production style-change recommendation.
212+
The aggregate Markdown summarizes source-layer coverage sums, top class coverage within each source layer, QGIS style-layer coverage sums, distinct QGIS runtimes, per-camera rows with class counts and class coverage, and ranked camera/class probe candidates from the input reports. Use it to choose the next owner-mask or missing-class probe across the camera matrix; it remains bbox attribution, not rendered-pixel ownership or a production style-change recommendation.
213213

214214
When source/crop overlap points at a possible rendered owner, run QGIS-only transparent layer masks against an existing comparison manifest before changing production paint:
215215

tests/test_mapbox_outdoors_source_crop_overlap.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,41 @@ def test_aggregate_report_summarizes_source_and_style_layer_coverage(self):
701701
source_rows["landuse"]["class_coverage"],
702702
{"park": 1.4, "residential": 1.25, "grass": 1.1},
703703
)
704+
self.assertEqual(
705+
aggregate["camera_class_rows"][:3],
706+
[
707+
{
708+
"input_report": str(geneva_report),
709+
"camera": "geneva-airport-motorway-z14-outdoors",
710+
"camera_zoom": 14.0,
711+
"source_layer": "landuse",
712+
"class": "park",
713+
"class_coverage": 1.4,
714+
"overlap_feature_count": 7,
715+
"qgis_style_layer_coverage": "landuse-green=1.500, landuse-other=0.250",
716+
},
717+
{
718+
"input_report": str(switzerland_report),
719+
"camera": "switzerland-alps-z5-outdoors",
720+
"camera_zoom": 5.0,
721+
"source_layer": "landuse",
722+
"class": "residential",
723+
"class_coverage": 1.25,
724+
"overlap_feature_count": 4,
725+
"qgis_style_layer_coverage": "landuse-green=0.750",
726+
},
727+
{
728+
"input_report": str(geneva_report),
729+
"camera": "geneva-airport-motorway-z14-outdoors",
730+
"camera_zoom": 14.0,
731+
"source_layer": "landuse",
732+
"class": "grass",
733+
"class_coverage": 1.1,
734+
"overlap_feature_count": 7,
735+
"qgis_style_layer_coverage": "landuse-green=1.500, landuse-other=0.250",
736+
},
737+
],
738+
)
704739
self.assertEqual(source_rows["aeroway"]["zero_overlap_reports"], 1)
705740
self.assertEqual(style_rows[("landuse", "landuse-green")]["feature_count"], 7)
706741
self.assertEqual(style_rows[("landuse", "landuse-green")]["coverage_sum"], 2.25)
@@ -718,8 +753,17 @@ def test_aggregate_report_summarizes_source_and_style_layer_coverage(self):
718753
)
719754
self.assertIn("Top source-layer bbox coverage sums: landuse=3.750", markdown)
720755
self.assertIn("Top source-layer class coverage sums: landuse: park=1.400", markdown)
756+
self.assertIn(
757+
"Top camera/class coverage candidates: geneva-airport-motorway-z14-outdoors landuse/park=1.400",
758+
markdown,
759+
)
721760
self.assertIn("Source layers with zero overlap wherever requested: aeroway.", markdown)
722761
self.assertIn("Treat aggregate coverage as bbox attribution across reports", markdown)
762+
self.assertIn("## Camera/class probe candidates", markdown)
763+
self.assertIn(
764+
"| `geneva-airport-motorway-z14-outdoors` | 14 | `landuse` | park | 1.400 | 7 | landuse-green=1.500, landuse-other=0.250 |",
765+
markdown,
766+
)
723767

724768
def test_main_aggregate_mode_writes_markdown_summary(self):
725769
with tempfile.TemporaryDirectory() as tmp:

validation/mapbox_outdoors_source_crop_overlap.py

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2213,6 +2213,41 @@ def _camera_source_row(
22132213
}
22142214

22152215

2216+
def _camera_class_rows(
2217+
*,
2218+
record: Mapping[str, object],
2219+
camera_name: str,
2220+
camera_zoom: object,
2221+
input_report: str,
2222+
) -> list[dict[str, object]]:
2223+
property_areas = record.get("property_overlap_areas")
2224+
if not isinstance(property_areas, Mapping):
2225+
return []
2226+
class_areas = property_areas.get("class")
2227+
if not isinstance(class_areas, Mapping):
2228+
return []
2229+
rows: list[dict[str, object]] = []
2230+
for class_name, area_record in class_areas.items():
2231+
if not isinstance(area_record, Mapping):
2232+
continue
2233+
coverage = _float_value(area_record.get("crop_coverage_ratio"))
2234+
if coverage <= 0.0:
2235+
continue
2236+
rows.append(
2237+
{
2238+
"input_report": input_report,
2239+
"camera": camera_name,
2240+
"camera_zoom": _float_value(camera_zoom),
2241+
"source_layer": str(record.get("source_layer") or MISSING_VALUE),
2242+
"class": str(class_name),
2243+
"class_coverage": _rounded_float(coverage),
2244+
"overlap_feature_count": _int_value(record.get("overlap_feature_count")),
2245+
"qgis_style_layer_coverage": _format_style_layer_matches(record),
2246+
}
2247+
)
2248+
return rows
2249+
2250+
22162251
def _source_layer_total_row(total: _AggregateSourceLayerTotal) -> dict[str, object]:
22172252
return {
22182253
"source_layer": total.source_layer,
@@ -2247,6 +2282,7 @@ def _aggregate_one_source_crop_report(
22472282
source_totals: dict[str, _AggregateSourceLayerTotal],
22482283
style_totals: dict[tuple[str, str], _AggregateStyleLayerTotal],
22492284
camera_rows: list[dict[str, object]],
2285+
camera_class_rows: list[dict[str, object]],
22502286
qgis_runtimes: set[str],
22512287
) -> str:
22522288
resolved_path = report_path.expanduser().resolve()
@@ -2277,6 +2313,14 @@ def _aggregate_one_source_crop_report(
22772313
input_report=input_report,
22782314
)
22792315
)
2316+
camera_class_rows.extend(
2317+
_camera_class_rows(
2318+
record=record,
2319+
camera_name=camera_name,
2320+
camera_zoom=report.get("camera_zoom"),
2321+
input_report=input_report,
2322+
)
2323+
)
22802324
return input_report
22812325

22822326

@@ -2317,6 +2361,21 @@ def _deduplicated_report_paths(report_paths: Sequence[Path]) -> list[Path]:
23172361
return unique_paths
23182362

23192363

2364+
def _sorted_camera_class_rows(rows: Sequence[Mapping[str, object]]) -> list[dict[str, object]]:
2365+
return [
2366+
dict(row)
2367+
for row in sorted(
2368+
rows,
2369+
key=lambda row: (
2370+
-float(row.get("class_coverage") or 0.0),
2371+
str(row.get("camera")),
2372+
str(row.get("source_layer")),
2373+
str(row.get("class")),
2374+
),
2375+
)
2376+
]
2377+
2378+
23202379
def build_source_crop_overlap_aggregate_report(
23212380
report_paths: Sequence[Path],
23222381
*,
@@ -2328,13 +2387,15 @@ def build_source_crop_overlap_aggregate_report(
23282387
source_totals: dict[str, _AggregateSourceLayerTotal] = {}
23292388
style_totals: dict[tuple[str, str], _AggregateStyleLayerTotal] = {}
23302389
camera_rows: list[dict[str, object]] = []
2390+
camera_class_rows: list[dict[str, object]] = []
23312391
qgis_runtimes: set[str] = set()
23322392
input_reports = [
23332393
_aggregate_one_source_crop_report(
23342394
report_path,
23352395
source_totals=source_totals,
23362396
style_totals=style_totals,
23372397
camera_rows=camera_rows,
2398+
camera_class_rows=camera_class_rows,
23382399
qgis_runtimes=qgis_runtimes,
23392400
)
23402401
for report_path in deduplicated_report_paths
@@ -2354,6 +2415,7 @@ def build_source_crop_overlap_aggregate_report(
23542415
str(row["source_layer"]),
23552416
),
23562417
),
2418+
"camera_class_rows": _sorted_camera_class_rows(camera_class_rows),
23572419
}
23582420

23592421

@@ -2420,6 +2482,20 @@ def _aggregate_camera_source_row(row: Mapping[str, object]) -> str:
24202482
)
24212483

24222484

2485+
def _aggregate_camera_class_row(row: Mapping[str, object]) -> str:
2486+
return _markdown_table_row(
2487+
[
2488+
f"`{row.get('camera')}`",
2489+
_format_camera_zoom(row.get("camera_zoom")),
2490+
f"`{row.get('source_layer')}`",
2491+
row.get("class") or "-",
2492+
_format_coverage(row.get("class_coverage")),
2493+
row.get("overlap_feature_count"),
2494+
row.get("qgis_style_layer_coverage") or "-",
2495+
]
2496+
)
2497+
2498+
24232499
def _aggregate_source_read_labels(rows: Sequence[Mapping[str, object]]) -> list[str]:
24242500
return [
24252501
(
@@ -2465,10 +2541,23 @@ def _aggregate_zero_overlap_labels(rows: Sequence[Mapping[str, object]]) -> list
24652541
][:5]
24662542

24672543

2544+
def _aggregate_camera_class_read_labels(rows: Sequence[Mapping[str, object]]) -> list[str]:
2545+
return [
2546+
(
2547+
f"{row.get('camera') or MISSING_VALUE} "
2548+
f"{row.get('source_layer') or MISSING_VALUE}/{row.get('class') or MISSING_VALUE}="
2549+
f"{float(row.get('class_coverage') or 0.0):.3f}"
2550+
)
2551+
for row in rows
2552+
if float(row.get("class_coverage") or 0.0) > 0.0
2553+
][:5]
2554+
2555+
24682556
def _aggregate_read_lines(
24692557
*,
24702558
source_rows: Sequence[Mapping[str, object]],
24712559
style_rows: Sequence[Mapping[str, object]],
2560+
camera_class_rows: Sequence[Mapping[str, object]],
24722561
) -> list[str]:
24732562
return [
24742563
"",
@@ -2477,6 +2566,7 @@ def _aggregate_read_lines(
24772566
f"- Top source-layer bbox coverage sums: {_joined_read_labels(_aggregate_source_read_labels(source_rows))}.",
24782567
f"- Top QGIS style-layer bbox coverage sums: {_joined_read_labels(_aggregate_style_read_labels(style_rows))}.",
24792568
f"- Top source-layer class coverage sums: {_joined_read_labels(_aggregate_class_read_labels(source_rows))}.",
2569+
f"- Top camera/class coverage candidates: {_joined_read_labels(_aggregate_camera_class_read_labels(camera_class_rows))}.",
24802570
(
24812571
"- Source layers with zero overlap wherever requested: "
24822572
f"{_joined_read_labels(_aggregate_zero_overlap_labels(source_rows))}."
@@ -2491,6 +2581,7 @@ def render_aggregate_markdown_summary(report: Mapping[str, object]) -> str:
24912581
source_rows = _list_of_mappings(report.get("source_layer_rows"))
24922582
style_rows = _list_of_mappings(report.get("style_layer_rows"))
24932583
camera_rows = _list_of_mappings(report.get("camera_source_rows"))
2584+
camera_class_rows = _list_of_mappings(report.get("camera_class_rows"))
24942585
lines = [
24952586
"# Mapbox Outdoors source/crop overlap aggregate",
24962587
"",
@@ -2503,7 +2594,13 @@ def render_aggregate_markdown_summary(report: Mapping[str, object]) -> str:
25032594
lines.extend(f"- `{path}`" for path in input_reports[:20])
25042595
if len(input_reports) > 20:
25052596
lines.append(f"- ... {len(input_reports) - 20} more")
2506-
lines.extend(_aggregate_read_lines(source_rows=source_rows, style_rows=style_rows))
2597+
lines.extend(
2598+
_aggregate_read_lines(
2599+
source_rows=source_rows,
2600+
style_rows=style_rows,
2601+
camera_class_rows=camera_class_rows,
2602+
)
2603+
)
25072604
lines.extend([
25082605
"",
25092606
"## Source layer totals",
@@ -2528,6 +2625,14 @@ def render_aggregate_markdown_summary(report: Mapping[str, object]) -> str:
25282625
"| --- | ---: | --- | ---: | ---: | --- | --- | --- |",
25292626
])
25302627
lines.extend(_aggregate_camera_source_row(row) for row in camera_rows) if camera_rows else lines.append("| _none_ | | | 0 | 0 | | | |")
2628+
lines.extend([
2629+
"",
2630+
"## Camera/class probe candidates",
2631+
"",
2632+
"| Camera | Zoom | Source layer | Class | Class coverage | Overlap features | QGIS style-layer coverage |",
2633+
"| --- | ---: | --- | --- | ---: | ---: | --- |",
2634+
])
2635+
lines.extend(_aggregate_camera_class_row(row) for row in camera_class_rows) if camera_class_rows else lines.append("| _none_ | | | | 0 | 0 | |")
25312636
lines.append("")
25322637
return "\n".join(lines)
25332638

0 commit comments

Comments
 (0)