fix(perception): reuse M4.8 for vegetation evidence

This commit is contained in:
DCCONSTRUCTIONS
2026-08-28 00:13:58 +03:00
parent 57204b3b0b
commit 67e6ae98e2
12 changed files with 482 additions and 226 deletions
@@ -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",
)
)