fix(perception): reuse M4.8 for vegetation evidence
This commit is contained in:
+126
-5
@@ -161,18 +161,81 @@ def find_goose_pairs(root: Path) -> list[tuple[Path, Path]]:
|
||||
return pairs
|
||||
|
||||
|
||||
def visual_indices(count: int, visual_count: int) -> set[int]:
|
||||
def visual_indices(count: int, visual_count: int) -> dict[int, dict[str, Any]]:
|
||||
if count <= 0 or visual_count <= 0:
|
||||
return set()
|
||||
return {}
|
||||
selected_count = min(count, visual_count)
|
||||
if selected_count == 1:
|
||||
return {0}
|
||||
return {0: {}}
|
||||
return {
|
||||
round(index * (count - 1) / (selected_count - 1))
|
||||
round(index * (count - 1) / (selected_count - 1)): {}
|
||||
for index in range(selected_count)
|
||||
}
|
||||
|
||||
|
||||
def truth_focused_visuals(
|
||||
items: list[tuple[str, Path, Path | None]],
|
||||
names: dict[int, str],
|
||||
contract: dict[str, Any],
|
||||
) -> dict[int, dict[str, Any]]:
|
||||
if contract.get("selection_basis") != "ground-truth-class-support-only":
|
||||
raise RunnerError("visual selection basis changed")
|
||||
case_count = contract.get("case_count")
|
||||
minimum_pixels = contract.get("minimum_focus_pixels")
|
||||
strata = contract.get("strata")
|
||||
if (
|
||||
not isinstance(case_count, int)
|
||||
or case_count <= 0
|
||||
or not isinstance(minimum_pixels, int)
|
||||
or minimum_pixels <= 0
|
||||
or not isinstance(strata, list)
|
||||
or sum(row.get("count", 0) for row in strata if isinstance(row, dict)) != case_count
|
||||
):
|
||||
raise RunnerError("visual case contract is invalid")
|
||||
ids_by_name = {class_name: label_id for label_id, class_name in names.items()}
|
||||
supports: list[dict[int, int]] = []
|
||||
for _, _, label_path in items:
|
||||
if label_path is None:
|
||||
raise RunnerError("truth-focused selection requires labels")
|
||||
truth = preprocess_label(Image.open(label_path))
|
||||
values, counts = np.unique(truth, return_counts=True)
|
||||
supports.append({int(value): int(count) for value, count in zip(values, counts)})
|
||||
|
||||
selected: dict[int, dict[str, Any]] = {}
|
||||
for raw in strata:
|
||||
if not isinstance(raw, dict):
|
||||
raise RunnerError("visual stratum is invalid")
|
||||
class_name = raw.get("class_name")
|
||||
count = raw.get("count")
|
||||
if class_name not in ids_by_name or not isinstance(count, int) or count <= 0:
|
||||
raise RunnerError("visual stratum identity changed")
|
||||
label_id = ids_by_name[class_name]
|
||||
ranked = sorted(
|
||||
(
|
||||
(support.get(label_id, 0), items[index][0], index)
|
||||
for index, support in enumerate(supports)
|
||||
if index not in selected and support.get(label_id, 0) > 0
|
||||
),
|
||||
key=lambda row: (-row[0], row[1]),
|
||||
)
|
||||
admitted = [row for row in ranked if row[0] >= minimum_pixels]
|
||||
if len(admitted) < count:
|
||||
admitted = ranked
|
||||
if len(admitted) < count:
|
||||
raise RunnerError(f"visual stratum {class_name} has fewer than {count} cases")
|
||||
for rank, (truth_pixels, _, index) in enumerate(admitted[:count], start=1):
|
||||
selected[index] = {
|
||||
"class_name": class_name,
|
||||
"label_id": label_id,
|
||||
"truth_pixels": truth_pixels,
|
||||
"truth_fraction": round(truth_pixels / float(512 * 512), 8),
|
||||
"stratum_rank": rank,
|
||||
}
|
||||
if len(selected) != case_count:
|
||||
raise RunnerError("truth-focused visual selection did not produce the frozen case count")
|
||||
return selected
|
||||
|
||||
|
||||
def load_model(candidate: str, checkpoint: Path) -> tuple[torch.nn.Module, str, list[str]]:
|
||||
failures: list[str] = []
|
||||
for model_name in MODEL_NAMES[candidate]:
|
||||
@@ -297,6 +360,9 @@ def write_visual_case(
|
||||
policy_palettes: dict[str, np.ndarray],
|
||||
crop_box: tuple[int, int, int, int],
|
||||
truth: np.ndarray | None = None,
|
||||
focus: dict[str, Any] | None = None,
|
||||
material_codes: np.ndarray | None = None,
|
||||
error_colors: dict[str, list[int]] | None = None,
|
||||
preserve_source_size: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
case_root = output / "cases" / case_id
|
||||
@@ -343,6 +409,26 @@ def write_visual_case(
|
||||
"relative_path": truth_semantic_path.relative_to(output).as_posix(),
|
||||
"sha256": save_image(truth_semantic_path, semantic_palette[truth_image], "RGBA"),
|
||||
}
|
||||
if material_codes is None or error_colors is None:
|
||||
raise RunnerError("truth visual case requires the material-error contract")
|
||||
truth_material = material_codes[truth_image]
|
||||
predicted_material = material_codes[prediction_image]
|
||||
truth_vegetation = truth_material > 0
|
||||
predicted_vegetation = predicted_material > 0
|
||||
error_overlay = np.zeros((*truth_image.shape, 4), dtype=np.uint8)
|
||||
correct = truth_vegetation & (truth_material == predicted_material)
|
||||
missed = truth_vegetation & ~predicted_vegetation
|
||||
false_positive = ~truth_vegetation & predicted_vegetation
|
||||
wrong_material = truth_vegetation & predicted_vegetation & (truth_material != predicted_material)
|
||||
error_overlay[correct] = error_colors["correct_material_rgba"]
|
||||
error_overlay[missed] = error_colors["missed_vegetation_rgba"]
|
||||
error_overlay[false_positive] = error_colors["false_vegetation_rgba"]
|
||||
error_overlay[wrong_material] = error_colors["wrong_vegetation_material_rgba"]
|
||||
error_path = case_root / "vegetation-material-error.png"
|
||||
files["vegetation_material_error"] = {
|
||||
"relative_path": error_path.relative_to(output).as_posix(),
|
||||
"sha256": save_image(error_path, error_overlay, "RGBA"),
|
||||
}
|
||||
return {
|
||||
"schema_version": VISUAL_SCHEMA,
|
||||
"case_id": case_id,
|
||||
@@ -350,6 +436,7 @@ def write_visual_case(
|
||||
"source_height": source_image.height,
|
||||
"center_crop_xyxy": list(crop_box),
|
||||
"outside_crop_state": "undefined" if preserve_source_size else "not-applicable",
|
||||
"focus": focus,
|
||||
"files": files,
|
||||
}
|
||||
|
||||
@@ -400,6 +487,27 @@ def run() -> None:
|
||||
)
|
||||
for preset in ("urban", "rural", "offroad")
|
||||
}
|
||||
provider_labels = provider_map["providers"]["goose-fine-64"]["labels"]
|
||||
vegetation_materials = sorted(
|
||||
{
|
||||
material
|
||||
for material in provider_labels.values()
|
||||
if material in {
|
||||
"grass",
|
||||
"herbaceous_vegetation",
|
||||
"cultivated_vegetation",
|
||||
"woody_shrub",
|
||||
"tree_or_trunk",
|
||||
"vegetation_unknown",
|
||||
}
|
||||
}
|
||||
)
|
||||
material_code_by_name = {
|
||||
material: index for index, material in enumerate(vegetation_materials, start=1)
|
||||
}
|
||||
material_codes = np.zeros(CLASS_COUNT, dtype=np.uint8)
|
||||
for label_id, class_name in names.items():
|
||||
material_codes[label_id] = material_code_by_name.get(provider_labels.get(class_name), 0)
|
||||
|
||||
if args.mode == "goose":
|
||||
pairs = find_goose_pairs(mapping_root)
|
||||
@@ -423,6 +531,17 @@ def run() -> None:
|
||||
if not items:
|
||||
raise RunnerError("no inputs were selected")
|
||||
|
||||
visual_contract = config.get("visual_case_contract")
|
||||
if not isinstance(visual_contract, dict):
|
||||
raise RunnerError("visual case contract is unavailable")
|
||||
configured_visual_count = visual_contract.get("case_count")
|
||||
if args.mode == "goose":
|
||||
if args.visual_count != configured_visual_count:
|
||||
raise RunnerError("GOOSE visual count differs from the truth-focused contract")
|
||||
selected_visuals = truth_focused_visuals(items, names, visual_contract)
|
||||
else:
|
||||
selected_visuals = visual_indices(len(items), args.visual_count)
|
||||
|
||||
args.output.mkdir(parents=True, exist_ok=False)
|
||||
torch.cuda.empty_cache()
|
||||
model, model_name, architecture_failures = load_model(args.candidate, args.checkpoint)
|
||||
@@ -430,7 +549,6 @@ def run() -> None:
|
||||
warmup_tensor, _ = preprocess(warmup_source)
|
||||
warmup_latencies_ms = [infer(model, warmup_tensor)[1] for _ in range(3)]
|
||||
torch.cuda.reset_peak_memory_stats()
|
||||
selected_visuals = visual_indices(len(items), args.visual_count)
|
||||
confusion = np.zeros((CLASS_COUNT, CLASS_COUNT), dtype=np.int64)
|
||||
latencies_ms: list[float] = []
|
||||
visuals: list[dict[str, Any]] = []
|
||||
@@ -454,6 +572,9 @@ def run() -> None:
|
||||
policy_palettes,
|
||||
crop_box,
|
||||
truth=truth,
|
||||
focus=selected_visuals[index] or None,
|
||||
material_codes=material_codes,
|
||||
error_colors=visual_contract["error_overlay"],
|
||||
preserve_source_size=args.mode == "ravnoves",
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user