Gate YOLO tile labels by visible ratio
This commit is contained in:
+6
-1
@@ -321,6 +321,7 @@ docker exec -it geointel python3 /app/scripts/export_operator_yolo_tile_dataset.
|
||||
--tile-size 160 \
|
||||
--stride 80 \
|
||||
--negative-keep-ratio 1.0 \
|
||||
--min-label-visible-ratio 0.25 \
|
||||
--val-samples turnhout,retie,kasterlee_bos \
|
||||
--force
|
||||
```
|
||||
@@ -329,6 +330,9 @@ The tile exporter clips GRB building bounding boxes into each tile, writes
|
||||
YOLO labels beside each tile image, keeps a deterministic ratio of empty
|
||||
negative tiles, and records `yolo_tile_dataset_summary.json` with
|
||||
`positive_tile_count`, `negative_tile_count` and skipped negative tile counts.
|
||||
`--min-label-visible-ratio` drops labels where only a small clipped fragment of
|
||||
the original building bbox is visible inside the tile; this reduces noisy
|
||||
tile-edge labels in overlapping-tile datasets. Use `0` for legacy behavior.
|
||||
It remains operator tooling only: no provider fetch, no API mutation and no
|
||||
automatic model training.
|
||||
|
||||
@@ -344,7 +348,8 @@ The audit reads the tile summary and YOLO label files, then writes
|
||||
`operator_yolo_dataset_quality_audit.json` and
|
||||
`operator_yolo_dataset_quality_audit.md`. It reports positive/background sample
|
||||
coverage, train/validation split coverage, repeated hard-negative pressure,
|
||||
missing or invalid label rows and normalized box-area signals. Treat
|
||||
minimum visible label ratio, missing or invalid label rows and normalized
|
||||
box-area signals. Treat
|
||||
`needs_attention` as a dataset-design warning, not as a runtime failure: the
|
||||
next action is usually more positive AOIs, better validation coverage or more
|
||||
unique hard negatives rather than simply extending epochs.
|
||||
|
||||
@@ -291,6 +291,7 @@ def build_audit(summary: dict[str, Any], summary_path: Path, args: argparse.Name
|
||||
"negative_keep_ratio": summary.get("negative_keep_ratio"),
|
||||
"background_negative_repeat": summary.get("background_negative_repeat"),
|
||||
"min_label_px": summary.get("min_label_px"),
|
||||
"min_label_visible_ratio": summary.get("min_label_visible_ratio"),
|
||||
"tile_count": len(tiles),
|
||||
"positive_tile_count": len(positive_tiles),
|
||||
"negative_tile_count": len(negative_tiles),
|
||||
@@ -348,6 +349,7 @@ def write_markdown(report: dict[str, Any], path: Path) -> None:
|
||||
f"- Tiles: {report['tile_count']} ({report['positive_tile_count']} positive, {report['negative_tile_count']} negative)",
|
||||
f"- Samples: {report['sample_count']} ({report['positive_sample_count']} positive, {report['background_sample_count']} background)",
|
||||
f"- Repeated background negative share: {report['repeated_background_negative_share_of_negatives']:.3f}",
|
||||
f"- Minimum visible label ratio: {format_optional_float(report.get('min_label_visible_ratio'))}",
|
||||
"",
|
||||
"## Label Quality",
|
||||
"",
|
||||
|
||||
@@ -78,6 +78,15 @@ def parse_args() -> argparse.Namespace:
|
||||
default=float(os.environ.get("OPERATOR_YOLO_MIN_LABEL_PX", "4")),
|
||||
help="Minimum clipped box width/height in pixels before a tile label is kept.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--min-label-visible-ratio",
|
||||
type=float,
|
||||
default=float(os.environ.get("OPERATOR_YOLO_MIN_LABEL_VISIBLE_RATIO", "0")),
|
||||
help=(
|
||||
"Minimum visible share of the original object bbox required before a clipped tile label is kept. "
|
||||
"Use 0 to keep legacy edge-fragment labels."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--background-negative-repeat",
|
||||
type=int,
|
||||
@@ -217,7 +226,7 @@ def load_reference_pixel_boxes(reference_path: Path, dataset: Any, min_label_px:
|
||||
return boxes
|
||||
|
||||
|
||||
def labels_for_tile(tile_window: TileWindow, boxes: list[PixelBox], min_label_px: float) -> list[str]:
|
||||
def labels_for_tile(tile_window: TileWindow, boxes: list[PixelBox], min_label_px: float, min_visible_ratio: float = 0.0) -> list[str]:
|
||||
labels: list[str] = []
|
||||
tile_min_col = tile_window.col_off
|
||||
tile_min_row = tile_window.row_off
|
||||
@@ -233,6 +242,11 @@ def labels_for_tile(tile_window: TileWindow, boxes: list[PixelBox], min_label_px
|
||||
box_height = max_row - min_row
|
||||
if box_width < min_label_px or box_height < min_label_px:
|
||||
continue
|
||||
original_area = max((box.max_col - box.min_col) * (box.max_row - box.min_row), 0.0)
|
||||
visible_area = box_width * box_height
|
||||
visible_ratio = visible_area / original_area if original_area > 0 else 0.0
|
||||
if min_visible_ratio > 0 and visible_ratio < min_visible_ratio:
|
||||
continue
|
||||
local_min_col = min_col - tile_min_col
|
||||
local_max_col = max_col - tile_min_col
|
||||
local_min_row = min_row - tile_min_row
|
||||
@@ -296,6 +310,7 @@ def export_sample_tiles(
|
||||
stride: int,
|
||||
negative_keep_ratio: float,
|
||||
min_label_px: float,
|
||||
min_label_visible_ratio: float,
|
||||
background_negative_repeat: int,
|
||||
) -> list[dict[str, Any]]:
|
||||
sample_slug = str(sample["sample_slug"])
|
||||
@@ -312,7 +327,12 @@ def export_sample_tiles(
|
||||
with rasterio.open(raster_path) as dataset:
|
||||
boxes = load_reference_pixel_boxes(reference_path, dataset, min_label_px=min_label_px)
|
||||
for tile_index, tile_window in enumerate(iter_tile_windows(dataset.width, dataset.height, tile_size, stride)):
|
||||
labels = labels_for_tile(tile_window, boxes, min_label_px=min_label_px)
|
||||
labels = labels_for_tile(
|
||||
tile_window,
|
||||
boxes,
|
||||
min_label_px=min_label_px,
|
||||
min_visible_ratio=min_label_visible_ratio,
|
||||
)
|
||||
is_negative = not labels
|
||||
if is_negative and not keep_negative_tile(sample_slug, tile_index, negative_keep_ratio):
|
||||
exported.append(
|
||||
@@ -391,6 +411,7 @@ def main() -> int:
|
||||
stride=args.stride,
|
||||
negative_keep_ratio=args.negative_keep_ratio,
|
||||
min_label_px=args.min_label_px,
|
||||
min_label_visible_ratio=args.min_label_visible_ratio,
|
||||
background_negative_repeat=args.background_negative_repeat,
|
||||
)
|
||||
)
|
||||
@@ -414,6 +435,7 @@ def main() -> int:
|
||||
"negative_keep_ratio": args.negative_keep_ratio,
|
||||
"background_negative_repeat": args.background_negative_repeat,
|
||||
"min_label_px": args.min_label_px,
|
||||
"min_label_visible_ratio": args.min_label_visible_ratio,
|
||||
"source_sample_count": len(samples),
|
||||
"tile_count": len(kept_tiles),
|
||||
"positive_tile_count": len(positive_tiles),
|
||||
|
||||
Reference in New Issue
Block a user