Balance YOLO label QA coverage
This commit is contained in:
+13
-1
@@ -488,7 +488,10 @@ The renderer writes `operator_yolo_label_qa_summary.json`,
|
||||
existing YOLO labels on existing tile images only; it does not run inference,
|
||||
train a model, fetch providers or create fake detections. Missing image files,
|
||||
missing label files, invalid YOLO rows and low-variance/blank-looking rendered
|
||||
tiles are reported in the JSON/Markdown artifacts.
|
||||
tiles are reported in the JSON/Markdown artifacts. Selection is balanced by
|
||||
`sample_slug` before taking additional high-density tiles, so one dense urban
|
||||
AOI cannot hide the other source samples from visual review. The report records
|
||||
`selected_sample_count` and `selected_sample_slugs` for coverage evidence.
|
||||
|
||||
Current Tower audit status:
|
||||
|
||||
@@ -518,6 +521,15 @@ Current Tower audit status:
|
||||
available but inactive. The split-aware promotion report rejected all tested
|
||||
thresholds: `0.25` and `0.35` passed the pure-empty background gate but had
|
||||
mean F1 below `0.25`; `0.15` also failed the pure-empty false-positive gate.
|
||||
- `yolo-building-aoi1024-expanded-minpx4vis035`: expanded small-building
|
||||
recovery dataset with 20 source AOIs, 171 retained tiles, 45,892 labels,
|
||||
144 train tiles, 27 validation tiles and 9 low-variance negatives removed.
|
||||
Its configured audit passed with no warnings, median normalized box area
|
||||
`0.000694274766`, small-box share `0.3832694151486098`, no invalid labels and
|
||||
no missing label files. The balanced visual pass rendered 40 tiles across all
|
||||
19 source samples that retained at least one tile, with no invalid labels,
|
||||
missing images or low-variance selections. A new candidate may be trained,
|
||||
but remains inactive until positive and split-background promotion gates pass.
|
||||
|
||||
After rebuilding the all-in-one image, the operator scripts are available inside
|
||||
the container at `/app/scripts/...`. Before rebuilding, use the host checkout or
|
||||
|
||||
@@ -120,6 +120,33 @@ def tile_sort_key(tile: dict[str, Any]) -> tuple[int, str, str, int, int]:
|
||||
)
|
||||
|
||||
|
||||
def balanced_tiles_by_sample(tiles: list[dict[str, Any]], limit: int) -> list[dict[str, Any]]:
|
||||
if limit <= 0 or not tiles:
|
||||
return []
|
||||
grouped: dict[str, list[dict[str, Any]]] = {}
|
||||
for tile in tiles:
|
||||
grouped.setdefault(str(tile.get("sample_slug") or "unknown"), []).append(tile)
|
||||
for sample_tiles in grouped.values():
|
||||
sample_tiles.sort(key=tile_sort_key)
|
||||
sample_order = sorted(grouped, key=lambda slug: tile_sort_key(grouped[slug][0]))
|
||||
|
||||
selected: list[dict[str, Any]] = []
|
||||
depth = 0
|
||||
while len(selected) < limit:
|
||||
added = False
|
||||
for slug in sample_order:
|
||||
sample_tiles = grouped[slug]
|
||||
if depth < len(sample_tiles):
|
||||
selected.append(sample_tiles[depth])
|
||||
added = True
|
||||
if len(selected) == limit:
|
||||
break
|
||||
if not added:
|
||||
break
|
||||
depth += 1
|
||||
return selected
|
||||
|
||||
|
||||
def select_tiles(tiles: list[dict[str, Any]], max_tiles: int) -> list[dict[str, Any]]:
|
||||
if max_tiles <= 0:
|
||||
raise ValueError("max_tiles must be positive")
|
||||
@@ -135,7 +162,8 @@ def select_tiles(tiles: list[dict[str, Any]], max_tiles: int) -> list[dict[str,
|
||||
)
|
||||
|
||||
negative_slots = min(len(negatives), max(1, max_tiles // 5)) if negatives and max_tiles > 1 else 0
|
||||
selected = positives[: max_tiles - negative_slots] + negatives[:negative_slots]
|
||||
selected = balanced_tiles_by_sample(positives, max_tiles - negative_slots)
|
||||
selected.extend(balanced_tiles_by_sample(negatives, negative_slots))
|
||||
|
||||
if len(selected) < max_tiles:
|
||||
selected_ids = {id(tile) for tile in selected}
|
||||
@@ -302,6 +330,12 @@ def build_report(summary: dict[str, Any], summary_path: Path, args: argparse.Nam
|
||||
"columns": args.columns,
|
||||
"thumb_size": args.thumb_size,
|
||||
"selected_tile_count": len(selected_tiles),
|
||||
"selected_sample_count": len(
|
||||
{str(tile.get("sample_slug") or "unknown") for tile in selected_tiles}
|
||||
),
|
||||
"selected_sample_slugs": sorted(
|
||||
{str(tile.get("sample_slug") or "unknown") for tile in selected_tiles}
|
||||
),
|
||||
"rendered_tile_count": len(rendered_cards),
|
||||
"missing_image_count": missing_image_count,
|
||||
"missing_label_file_count": missing_label_file_count,
|
||||
@@ -322,6 +356,7 @@ def write_markdown(report: dict[str, Any], output_dir: Path) -> None:
|
||||
"",
|
||||
f"- status: `{report['status']}`",
|
||||
f"- selected tiles: {report['selected_tile_count']}",
|
||||
f"- selected source samples: {report['selected_sample_count']}",
|
||||
f"- rendered tiles: {report['rendered_tile_count']}",
|
||||
f"- missing images: {report['missing_image_count']}",
|
||||
f"- missing label files: {report['missing_label_file_count']}",
|
||||
|
||||
Reference in New Issue
Block a user