feat(perception): add PointPillars transfer gate
This commit is contained in:
@@ -38,6 +38,14 @@ from k1link.datasets.goose_qualification import (
|
||||
GroundAcceptancePolicy,
|
||||
qualify_goose_ground,
|
||||
)
|
||||
from k1link.datasets.kitti_3d_admission import (
|
||||
KITTI_3D_ADMISSION_SCHEMA,
|
||||
KITTI_3D_SOURCE_ID,
|
||||
Kitti3DAdmissionError,
|
||||
admit_kitti_3d_object_release,
|
||||
read_kitti_3d_admission,
|
||||
read_kitti_standard_splits,
|
||||
)
|
||||
from k1link.datasets.rellis_admission import (
|
||||
RELLIS_ADMISSION_SCHEMA,
|
||||
RellisAdmissionError,
|
||||
@@ -79,6 +87,8 @@ __all__ = [
|
||||
"GOOSE_QUALIFICATION_PREVIEW_SCHEMA",
|
||||
"GOOSE_QUALIFICATION_PROFILE_SCHEMA",
|
||||
"GOOSE_QUALIFICATION_REPORT_SCHEMA",
|
||||
"KITTI_3D_ADMISSION_SCHEMA",
|
||||
"KITTI_3D_SOURCE_ID",
|
||||
"RELLIS_CLASSES",
|
||||
"RELLIS_ADMISSION_SCHEMA",
|
||||
"RELLIS_GROUND_POLICY_SCHEMA",
|
||||
@@ -91,6 +101,7 @@ __all__ = [
|
||||
"RellisAdmissionError",
|
||||
"RellisPatchworkProfile",
|
||||
"RellisSmokeError",
|
||||
"Kitti3DAdmissionError",
|
||||
"GoosePatchworkProfile",
|
||||
"GroundAcceptancePolicy",
|
||||
"DegradationProfile",
|
||||
@@ -98,6 +109,7 @@ __all__ = [
|
||||
"benchmark_goose_current_ground",
|
||||
"benchmark_goose_patchwork_ground",
|
||||
"admit_rellis_release",
|
||||
"admit_kitti_3d_object_release",
|
||||
"build_rellis_official_smoke_preview",
|
||||
"calibrate_rellis_sensor_height",
|
||||
"configured_dataset_admission_manifest",
|
||||
@@ -108,6 +120,8 @@ __all__ = [
|
||||
"read_dataset_admission_manifest",
|
||||
"read_dataset_ground_preview",
|
||||
"read_dataset_native_scan_preview",
|
||||
"read_kitti_3d_admission",
|
||||
"read_kitti_standard_splits",
|
||||
"read_semantic_kitti_frame",
|
||||
"rellis_native_scan_preview",
|
||||
"qualify_rellis_ground",
|
||||
|
||||
@@ -15,6 +15,10 @@ from k1link.datasets.goose_benchmark import (
|
||||
)
|
||||
from k1link.datasets.goose_qualification import qualify_goose_ground
|
||||
from k1link.datasets.goose_review import build_goose_ground_review_pack
|
||||
from k1link.datasets.kitti_3d_admission import (
|
||||
Kitti3DAdmissionError,
|
||||
admit_kitti_3d_object_release,
|
||||
)
|
||||
from k1link.datasets.rellis_admission import RellisAdmissionError, admit_rellis_release
|
||||
from k1link.datasets.rellis_qualification import qualify_rellis_ground
|
||||
from k1link.datasets.rellis_smoke import (
|
||||
@@ -28,6 +32,23 @@ app = typer.Typer(
|
||||
)
|
||||
|
||||
|
||||
@app.command("admit-kitti-3d-object")
|
||||
def admit_kitti_3d_object_command(
|
||||
dataset_root: Annotated[
|
||||
Path,
|
||||
typer.Option("--dataset-root", exists=True, file_okay=False, resolve_path=True),
|
||||
],
|
||||
) -> None:
|
||||
"""Verify the archive-only KITTI 3D release and standard validation split."""
|
||||
|
||||
try:
|
||||
manifest = admit_kitti_3d_object_release(dataset_root)
|
||||
except Kitti3DAdmissionError as exc:
|
||||
typer.echo(str(exc), err=True)
|
||||
raise typer.Exit(code=2) from exc
|
||||
typer.echo(json.dumps(manifest, ensure_ascii=False, sort_keys=True))
|
||||
|
||||
|
||||
@app.command("admit-goose-validation")
|
||||
def admit_goose_validation_command(
|
||||
dataset_root: Annotated[
|
||||
|
||||
@@ -0,0 +1,552 @@
|
||||
"""Fail-closed admission of the KITTI 3D object development release.
|
||||
|
||||
The release is admitted as independent oriented-3D-box truth for the L3
|
||||
PointPillars baseline. Source archives stay on Worker 006. This module does
|
||||
not extract data, install a model, run inference, or authorize a K1 quality
|
||||
claim.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
import zipfile
|
||||
from collections import Counter
|
||||
from contextlib import suppress
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import Any, Final
|
||||
|
||||
KITTI_3D_ADMISSION_SCHEMA: Final = "missioncore.kitti-3d-object-admission/v1"
|
||||
KITTI_3D_SOURCE_ID: Final = "kitti-3d-object/v2017"
|
||||
KITTI_3D_RELEASE_ROOT: Final = "kitti-3d-object/v2017"
|
||||
KITTI_3D_LICENSE: Final = "CC-BY-NC-SA-3.0"
|
||||
KITTI_3D_LICENSE_URL: Final = "https://www.cvlibs.net/datasets/kitti/index.php"
|
||||
KITTI_3D_BENCHMARK_URL: Final = (
|
||||
"https://www.cvlibs.net/datasets/kitti/eval_object.php?obj_benchmark=3d"
|
||||
)
|
||||
|
||||
KITTI_VELODYNE_ARCHIVE: Final = "data_object_velodyne.zip"
|
||||
KITTI_LABEL_ARCHIVE: Final = "data_object_label_2.zip"
|
||||
KITTI_CALIB_ARCHIVE: Final = "data_object_calib.zip"
|
||||
KITTI_ARCHIVE_URLS: Final[dict[str, str]] = {
|
||||
KITTI_VELODYNE_ARCHIVE: (
|
||||
"https://s3.eu-central-1.amazonaws.com/avg-kitti/data_object_velodyne.zip"
|
||||
),
|
||||
KITTI_LABEL_ARCHIVE: (
|
||||
"https://s3.eu-central-1.amazonaws.com/avg-kitti/data_object_label_2.zip"
|
||||
),
|
||||
KITTI_CALIB_ARCHIVE: (
|
||||
"https://s3.eu-central-1.amazonaws.com/avg-kitti/data_object_calib.zip"
|
||||
),
|
||||
}
|
||||
KITTI_ARCHIVE_BYTES: Final[dict[str, int]] = {
|
||||
KITTI_VELODYNE_ARCHIVE: 28_750_710_812,
|
||||
KITTI_LABEL_ARCHIVE: 5_601_213,
|
||||
KITTI_CALIB_ARCHIVE: 26_854_811,
|
||||
}
|
||||
|
||||
KITTI_TRAINING_FRAMES: Final = 7_481
|
||||
KITTI_TEST_FRAMES: Final = 7_518
|
||||
KITTI_TARGET_CLASSES: Final = ("Car", "Pedestrian", "Cyclist")
|
||||
KITTI_STANDARD_SPLIT_COMMIT: Final = (
|
||||
"233f849829b6ac19afb8af8837a0246890908755"
|
||||
)
|
||||
KITTI_STANDARD_SPLIT_URL: Final = (
|
||||
"https://github.com/open-mmlab/OpenPCDet/tree/"
|
||||
f"{KITTI_STANDARD_SPLIT_COMMIT}/data/kitti/ImageSets"
|
||||
)
|
||||
KITTI_SPLIT_FILES: Final[dict[str, str]] = {
|
||||
"train": "train.txt",
|
||||
"validation": "val.txt",
|
||||
}
|
||||
KITTI_SPLIT_COUNTS: Final[dict[str, int]] = {
|
||||
"train": 3_712,
|
||||
"validation": 3_769,
|
||||
}
|
||||
KITTI_SPLIT_SHA256: Final[dict[str, str]] = {
|
||||
"train": "b6417a1d9b18c8fdb085128e633d28ff321b7674a6d1b3841b8f43d865b281cb",
|
||||
"validation": (
|
||||
"657ac4bcc1e156e5b106a4ca18e1f88e012787ea1d2b5d0adeea97fee903fa86"
|
||||
),
|
||||
}
|
||||
|
||||
MAX_ARCHIVE_ENTRIES: Final = 40_000
|
||||
MAX_UNCOMPRESSED_BYTES: Final = 256 * 1024**3
|
||||
MAX_LABEL_MEMBER_BYTES: Final = 8 * 1024**2
|
||||
_FRAME_ID = re.compile(r"^[0-9]{6}$")
|
||||
_VELODYNE_MEMBER = re.compile(
|
||||
r"^(?P<split>training|testing)/velodyne/(?P<frame>[0-9]{6})\.bin$"
|
||||
)
|
||||
_LABEL_MEMBER = re.compile(r"^training/label_2/(?P<frame>[0-9]{6})\.txt$")
|
||||
_CALIB_MEMBER = re.compile(
|
||||
r"^(?P<split>training|testing)/calib/(?P<frame>[0-9]{6})\.txt$"
|
||||
)
|
||||
_CALIB_KEYS: Final = {
|
||||
"P0",
|
||||
"P1",
|
||||
"P2",
|
||||
"P3",
|
||||
"R0_rect",
|
||||
"Tr_velo_to_cam",
|
||||
"Tr_imu_to_velo",
|
||||
}
|
||||
|
||||
|
||||
class Kitti3DAdmissionError(RuntimeError):
|
||||
"""The KITTI 3D development release violates its pinned contract."""
|
||||
|
||||
|
||||
def admit_kitti_3d_object_release(
|
||||
dataset_root: Path,
|
||||
*,
|
||||
velodyne_archive: Path | None = None,
|
||||
label_archive: Path | None = None,
|
||||
calib_archive: Path | None = None,
|
||||
train_split: Path | None = None,
|
||||
validation_split: Path | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Verify the archive-only KITTI release and publish a path-free state."""
|
||||
|
||||
root = dataset_root.expanduser().absolute()
|
||||
if not _is_worker_dataset_root(root):
|
||||
raise Kitti3DAdmissionError(
|
||||
"KITTI admission requires the canonical Worker 006 D dataset root"
|
||||
)
|
||||
archive_root = root / KITTI_3D_RELEASE_ROOT / "archives"
|
||||
split_root = root / KITTI_3D_RELEASE_ROOT / "splits" / (
|
||||
f"openpcdet-{KITTI_STANDARD_SPLIT_COMMIT}"
|
||||
)
|
||||
archive_paths = {
|
||||
KITTI_VELODYNE_ARCHIVE: _resolved_input(
|
||||
archive_root, velodyne_archive, KITTI_VELODYNE_ARCHIVE
|
||||
),
|
||||
KITTI_LABEL_ARCHIVE: _resolved_input(
|
||||
archive_root, label_archive, KITTI_LABEL_ARCHIVE
|
||||
),
|
||||
KITTI_CALIB_ARCHIVE: _resolved_input(
|
||||
archive_root, calib_archive, KITTI_CALIB_ARCHIVE
|
||||
),
|
||||
}
|
||||
split_paths = {
|
||||
"train": _resolved_input(
|
||||
split_root, train_split, KITTI_SPLIT_FILES["train"]
|
||||
),
|
||||
"validation": _resolved_input(
|
||||
split_root, validation_split, KITTI_SPLIT_FILES["validation"]
|
||||
),
|
||||
}
|
||||
if any(not path.is_file() for path in (*archive_paths.values(), *split_paths.values())):
|
||||
raise Kitti3DAdmissionError("one or more pinned KITTI artifacts are unavailable")
|
||||
|
||||
archives: dict[str, dict[str, Any]] = {}
|
||||
for filename, path in archive_paths.items():
|
||||
size_bytes = path.stat().st_size
|
||||
if size_bytes != KITTI_ARCHIVE_BYTES[filename]:
|
||||
raise Kitti3DAdmissionError(
|
||||
f"{filename} size differs from the pinned KITTI release"
|
||||
)
|
||||
archives[filename] = {
|
||||
"filename": filename,
|
||||
"source_url": KITTI_ARCHIVE_URLS[filename],
|
||||
"size_bytes": size_bytes,
|
||||
"sha256": _sha256_file(path),
|
||||
"vendor_checksum_available": False,
|
||||
}
|
||||
|
||||
splits = _read_standard_splits(split_paths)
|
||||
try:
|
||||
with (
|
||||
zipfile.ZipFile(archive_paths[KITTI_VELODYNE_ARCHIVE]) as points_zip,
|
||||
zipfile.ZipFile(archive_paths[KITTI_LABEL_ARCHIVE]) as labels_zip,
|
||||
zipfile.ZipFile(archive_paths[KITTI_CALIB_ARCHIVE]) as calib_zip,
|
||||
):
|
||||
point_members = _member_index(points_zip)
|
||||
label_members = _member_index(labels_zip)
|
||||
calib_members = _member_index(calib_zip)
|
||||
training_points, testing_points = _validate_velodyne(point_members)
|
||||
training_labels, target_counts = _validate_labels(
|
||||
labels_zip, label_members
|
||||
)
|
||||
training_calib, testing_calib = _validate_calibrations(
|
||||
calib_zip, calib_members
|
||||
)
|
||||
except (OSError, KeyError, UnicodeDecodeError, zipfile.BadZipFile) as exc:
|
||||
raise Kitti3DAdmissionError("KITTI archives could not be verified") from exc
|
||||
|
||||
training_ids = set(training_points)
|
||||
if (
|
||||
set(training_labels) != training_ids
|
||||
or set(training_calib) != training_ids
|
||||
or set(testing_points) != set(testing_calib)
|
||||
):
|
||||
raise Kitti3DAdmissionError("KITTI point, label, and calibration indices diverge")
|
||||
split_union = set(splits["train"]) | set(splits["validation"])
|
||||
if (
|
||||
set(splits["train"]).intersection(splits["validation"])
|
||||
or split_union != training_ids
|
||||
):
|
||||
raise Kitti3DAdmissionError(
|
||||
"OpenPCDet train/validation split is overlapping or incomplete"
|
||||
)
|
||||
validation_target_counts = _target_counts_for_frames(
|
||||
archive_paths[KITTI_LABEL_ARCHIVE],
|
||||
set(splits["validation"]),
|
||||
)
|
||||
if any(validation_target_counts[class_name] <= 0 for class_name in KITTI_TARGET_CLASSES):
|
||||
raise Kitti3DAdmissionError("KITTI validation split lacks a target class")
|
||||
|
||||
identity = {
|
||||
"source_id": KITTI_3D_SOURCE_ID,
|
||||
"archives": archives,
|
||||
"split_source": {
|
||||
"repository_commit": KITTI_STANDARD_SPLIT_COMMIT,
|
||||
"source_url": KITTI_STANDARD_SPLIT_URL,
|
||||
"sha256": KITTI_SPLIT_SHA256,
|
||||
},
|
||||
"license": {
|
||||
"spdx": KITTI_3D_LICENSE,
|
||||
"source_url": KITTI_3D_LICENSE_URL,
|
||||
"use_scope": "academic-non-commercial",
|
||||
},
|
||||
"representation": {
|
||||
"point_fields": ["x", "y", "z", "intensity"],
|
||||
"ground_truth": "oriented-3d-boxes",
|
||||
"box_coordinate_frame": "camera-rectified",
|
||||
"calibration_to_sensor_frame_present": True,
|
||||
},
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
manifest = {
|
||||
"schema_version": KITTI_3D_ADMISSION_SCHEMA,
|
||||
"source_id": KITTI_3D_SOURCE_ID,
|
||||
"observed_at_utc": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
|
||||
"status": "archive-ready",
|
||||
"release_identity_sha256": identity_sha256,
|
||||
"storage": {
|
||||
"policy": "worker-d-only",
|
||||
"admitted": True,
|
||||
"canonical_root": True,
|
||||
"path_exposed": False,
|
||||
"source_archives_extracted": False,
|
||||
},
|
||||
"identity": identity,
|
||||
"alignment": {
|
||||
"training_frame_count": len(training_points),
|
||||
"test_frame_count": len(testing_points),
|
||||
"training_label_count": len(training_labels),
|
||||
"training_calibration_count": len(training_calib),
|
||||
"test_calibration_count": len(testing_calib),
|
||||
"split_counts": KITTI_SPLIT_COUNTS,
|
||||
"split_union_complete": True,
|
||||
"split_overlap_count": 0,
|
||||
"all_target_box_counts": dict(sorted(target_counts.items())),
|
||||
"validation_target_box_counts": dict(
|
||||
sorted(validation_target_counts.items())
|
||||
),
|
||||
},
|
||||
"benchmark_contract": {
|
||||
"independent_ground_truth": True,
|
||||
"annotations": ["oriented-3d-boxes"],
|
||||
"point_fields": ["x", "y", "z", "intensity"],
|
||||
"eligible_split": "validation",
|
||||
"target_classes": list(KITTI_TARGET_CLASSES),
|
||||
"official_test_submission_authorized": False,
|
||||
"retuning_on_validation_allowed": False,
|
||||
"k1_quality_claim_authorized": False,
|
||||
},
|
||||
"next_action": "promote-staged-engine-then-stream-standard-validation",
|
||||
}
|
||||
_atomic_json(root / "state/kitti-3d-object-v2017.json", manifest)
|
||||
return manifest
|
||||
|
||||
|
||||
def read_kitti_3d_admission(dataset_root: Path) -> dict[str, Any]:
|
||||
"""Read the current path-free state and validate its content identity."""
|
||||
|
||||
root = dataset_root.expanduser().absolute()
|
||||
if not _is_worker_dataset_root(root):
|
||||
raise Kitti3DAdmissionError(
|
||||
"KITTI admission requires the canonical Worker 006 D dataset root"
|
||||
)
|
||||
path = root / "state/kitti-3d-object-v2017.json"
|
||||
try:
|
||||
manifest = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
raise Kitti3DAdmissionError("KITTI admission state is unavailable") from exc
|
||||
if not isinstance(manifest, dict):
|
||||
raise Kitti3DAdmissionError("KITTI admission state is not an object")
|
||||
identity = manifest.get("identity")
|
||||
if (
|
||||
manifest.get("schema_version") != KITTI_3D_ADMISSION_SCHEMA
|
||||
or manifest.get("source_id") != KITTI_3D_SOURCE_ID
|
||||
or manifest.get("status") != "archive-ready"
|
||||
or not isinstance(identity, dict)
|
||||
or manifest.get("release_identity_sha256")
|
||||
!= hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
):
|
||||
raise Kitti3DAdmissionError("KITTI admission state identity is invalid")
|
||||
return manifest
|
||||
|
||||
|
||||
def read_kitti_standard_splits(
|
||||
dataset_root: Path,
|
||||
) -> dict[str, tuple[str, ...]]:
|
||||
"""Read the pinned OpenPCDet split files after validating current state."""
|
||||
|
||||
root = dataset_root.expanduser().absolute()
|
||||
read_kitti_3d_admission(root)
|
||||
split_root = root / KITTI_3D_RELEASE_ROOT / "splits" / (
|
||||
f"openpcdet-{KITTI_STANDARD_SPLIT_COMMIT}"
|
||||
)
|
||||
return _read_standard_splits(
|
||||
{
|
||||
split_name: split_root / filename
|
||||
for split_name, filename in KITTI_SPLIT_FILES.items()
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _resolved_input(root: Path, provided: Path | None, filename: str) -> Path:
|
||||
return provided.expanduser().absolute() if provided is not None else root / filename
|
||||
|
||||
|
||||
def _is_worker_dataset_root(root: Path) -> bool:
|
||||
normalized = str(root).replace("\\", "/").rstrip("/").lower()
|
||||
return normalized == "/mnt/d/ndc_missioncore/datasets"
|
||||
|
||||
|
||||
def _sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
try:
|
||||
with path.open("rb") as source:
|
||||
for chunk in iter(lambda: source.read(8 * 1024**2), b""):
|
||||
digest.update(chunk)
|
||||
except OSError as exc:
|
||||
raise Kitti3DAdmissionError("KITTI artifact cannot be hashed") from exc
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _read_standard_splits(paths: dict[str, Path]) -> dict[str, tuple[str, ...]]:
|
||||
result: dict[str, tuple[str, ...]] = {}
|
||||
for split_name, path in paths.items():
|
||||
if _sha256_file(path) != KITTI_SPLIT_SHA256[split_name]:
|
||||
raise Kitti3DAdmissionError(
|
||||
f"KITTI {split_name} split differs from the pinned OpenPCDet commit"
|
||||
)
|
||||
try:
|
||||
rows = tuple(
|
||||
row.strip()
|
||||
for row in path.read_text(encoding="ascii").splitlines()
|
||||
if row.strip()
|
||||
)
|
||||
except (OSError, UnicodeDecodeError) as exc:
|
||||
raise Kitti3DAdmissionError("KITTI split cannot be read") from exc
|
||||
if (
|
||||
len(rows) != KITTI_SPLIT_COUNTS[split_name]
|
||||
or len(set(rows)) != len(rows)
|
||||
or any(_FRAME_ID.fullmatch(row) is None for row in rows)
|
||||
):
|
||||
raise Kitti3DAdmissionError(f"KITTI {split_name} split is invalid")
|
||||
result[split_name] = rows
|
||||
return result
|
||||
|
||||
|
||||
def _member_index(source: zipfile.ZipFile) -> dict[str, zipfile.ZipInfo]:
|
||||
members = source.infolist()
|
||||
if not members or len(members) > MAX_ARCHIVE_ENTRIES:
|
||||
raise Kitti3DAdmissionError("KITTI archive entry count is invalid")
|
||||
total_uncompressed = 0
|
||||
indexed: dict[str, zipfile.ZipInfo] = {}
|
||||
for member in members:
|
||||
path = PurePosixPath(member.filename)
|
||||
if (
|
||||
path.is_absolute()
|
||||
or ".." in path.parts
|
||||
or "\\" in member.filename
|
||||
or member.file_size < 0
|
||||
or member.compress_size < 0
|
||||
):
|
||||
raise Kitti3DAdmissionError("KITTI archive contains an unsafe member")
|
||||
total_uncompressed += member.file_size
|
||||
if total_uncompressed > MAX_UNCOMPRESSED_BYTES:
|
||||
raise Kitti3DAdmissionError("KITTI archive expands beyond the admitted limit")
|
||||
if member.is_dir():
|
||||
continue
|
||||
normalized = path.as_posix()
|
||||
if normalized in indexed:
|
||||
raise Kitti3DAdmissionError("KITTI archive contains duplicate members")
|
||||
indexed[normalized] = member
|
||||
return indexed
|
||||
|
||||
|
||||
def _validate_velodyne(
|
||||
members: dict[str, zipfile.ZipInfo],
|
||||
) -> tuple[dict[str, zipfile.ZipInfo], dict[str, zipfile.ZipInfo]]:
|
||||
indexed: dict[str, dict[str, zipfile.ZipInfo]] = {
|
||||
"training": {},
|
||||
"testing": {},
|
||||
}
|
||||
for path, member in members.items():
|
||||
match = _VELODYNE_MEMBER.fullmatch(path)
|
||||
if match is None:
|
||||
continue
|
||||
if member.file_size <= 0 or member.file_size % 16:
|
||||
raise Kitti3DAdmissionError("KITTI Velodyne frame is not packed XYZI")
|
||||
indexed[match.group("split")][match.group("frame")] = member
|
||||
if (
|
||||
len(indexed["training"]) != KITTI_TRAINING_FRAMES
|
||||
or len(indexed["testing"]) != KITTI_TEST_FRAMES
|
||||
):
|
||||
raise Kitti3DAdmissionError("KITTI Velodyne frame count is invalid")
|
||||
return indexed["training"], indexed["testing"]
|
||||
|
||||
|
||||
def _validate_labels(
|
||||
source: zipfile.ZipFile,
|
||||
members: dict[str, zipfile.ZipInfo],
|
||||
) -> tuple[dict[str, zipfile.ZipInfo], Counter[str]]:
|
||||
indexed: dict[str, zipfile.ZipInfo] = {}
|
||||
counts: Counter[str] = Counter()
|
||||
for path, member in members.items():
|
||||
match = _LABEL_MEMBER.fullmatch(path)
|
||||
if match is None:
|
||||
continue
|
||||
if member.file_size > MAX_LABEL_MEMBER_BYTES:
|
||||
raise Kitti3DAdmissionError("KITTI label member is unexpectedly large")
|
||||
frame_id = match.group("frame")
|
||||
indexed[frame_id] = member
|
||||
counts.update(_parse_label_member(source.read(member)))
|
||||
if len(indexed) != KITTI_TRAINING_FRAMES:
|
||||
raise Kitti3DAdmissionError("KITTI label frame count is invalid")
|
||||
if any(counts[class_name] <= 0 for class_name in KITTI_TARGET_CLASSES):
|
||||
raise Kitti3DAdmissionError("KITTI release lacks a target 3D box class")
|
||||
return indexed, counts
|
||||
|
||||
|
||||
def _parse_label_member(payload: bytes) -> Counter[str]:
|
||||
try:
|
||||
text = payload.decode("ascii")
|
||||
except UnicodeDecodeError as exc:
|
||||
raise Kitti3DAdmissionError("KITTI label member is not ASCII") from exc
|
||||
counts: Counter[str] = Counter()
|
||||
for raw_line in text.splitlines():
|
||||
fields = raw_line.split()
|
||||
if not fields:
|
||||
continue
|
||||
if len(fields) != 15:
|
||||
raise Kitti3DAdmissionError("KITTI label row does not have 15 fields")
|
||||
class_name = fields[0]
|
||||
try:
|
||||
values = [float(value) for value in fields[1:]]
|
||||
except ValueError as exc:
|
||||
raise Kitti3DAdmissionError("KITTI label row contains invalid numbers") from exc
|
||||
if not all(math.isfinite(value) for value in values):
|
||||
raise Kitti3DAdmissionError("KITTI label row contains non-finite numbers")
|
||||
if class_name in KITTI_TARGET_CLASSES:
|
||||
height, width, length = values[7:10]
|
||||
if height <= 0 or width <= 0 or length <= 0:
|
||||
raise Kitti3DAdmissionError("KITTI target box has invalid dimensions")
|
||||
counts[class_name] += 1
|
||||
return counts
|
||||
|
||||
|
||||
def _validate_calibrations(
|
||||
source: zipfile.ZipFile,
|
||||
members: dict[str, zipfile.ZipInfo],
|
||||
) -> tuple[dict[str, zipfile.ZipInfo], dict[str, zipfile.ZipInfo]]:
|
||||
indexed: dict[str, dict[str, zipfile.ZipInfo]] = {
|
||||
"training": {},
|
||||
"testing": {},
|
||||
}
|
||||
for path, member in members.items():
|
||||
match = _CALIB_MEMBER.fullmatch(path)
|
||||
if match is None:
|
||||
continue
|
||||
payload = source.read(member)
|
||||
try:
|
||||
lines = payload.decode("ascii").splitlines()
|
||||
except UnicodeDecodeError as exc:
|
||||
raise Kitti3DAdmissionError("KITTI calibration is not ASCII") from exc
|
||||
keys: set[str] = set()
|
||||
for line in lines:
|
||||
if not line.strip():
|
||||
continue
|
||||
key, separator, raw_values = line.partition(":")
|
||||
if not separator:
|
||||
raise Kitti3DAdmissionError("KITTI calibration row is invalid")
|
||||
try:
|
||||
values = [float(value) for value in raw_values.split()]
|
||||
except ValueError as exc:
|
||||
raise Kitti3DAdmissionError(
|
||||
"KITTI calibration contains invalid numbers"
|
||||
) from exc
|
||||
if not values or not all(math.isfinite(value) for value in values):
|
||||
raise Kitti3DAdmissionError(
|
||||
"KITTI calibration contains non-finite numbers"
|
||||
)
|
||||
keys.add(key)
|
||||
if not _CALIB_KEYS.issubset(keys):
|
||||
raise Kitti3DAdmissionError("KITTI calibration lacks required transforms")
|
||||
indexed[match.group("split")][match.group("frame")] = member
|
||||
if (
|
||||
len(indexed["training"]) != KITTI_TRAINING_FRAMES
|
||||
or len(indexed["testing"]) != KITTI_TEST_FRAMES
|
||||
):
|
||||
raise Kitti3DAdmissionError("KITTI calibration frame count is invalid")
|
||||
return indexed["training"], indexed["testing"]
|
||||
|
||||
|
||||
def _target_counts_for_frames(
|
||||
labels_archive: Path,
|
||||
frame_ids: set[str],
|
||||
) -> Counter[str]:
|
||||
counts: Counter[str] = Counter()
|
||||
try:
|
||||
with zipfile.ZipFile(labels_archive) as source:
|
||||
members = _member_index(source)
|
||||
for frame_id in sorted(frame_ids):
|
||||
path = f"training/label_2/{frame_id}.txt"
|
||||
member = members.get(path)
|
||||
if member is None:
|
||||
raise Kitti3DAdmissionError(
|
||||
"KITTI validation split references a missing label"
|
||||
)
|
||||
counts.update(_parse_label_member(source.read(member)))
|
||||
except (OSError, zipfile.BadZipFile) as exc:
|
||||
raise Kitti3DAdmissionError(
|
||||
"KITTI validation labels could not be verified"
|
||||
) from exc
|
||||
return counts
|
||||
|
||||
|
||||
def _canonical_json(payload: Any) -> bytes:
|
||||
return json.dumps(
|
||||
payload,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def _atomic_json(path: Path, payload: dict[str, Any]) -> None:
|
||||
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
descriptor, temporary = tempfile.mkstemp(
|
||||
dir=path.parent,
|
||||
prefix=f".{path.name}.",
|
||||
suffix=".tmp",
|
||||
)
|
||||
try:
|
||||
with os.fdopen(descriptor, "wb") as target:
|
||||
target.write(_canonical_json(payload) + b"\n")
|
||||
target.flush()
|
||||
os.fsync(target.fileno())
|
||||
os.replace(temporary, path)
|
||||
except BaseException:
|
||||
with suppress(OSError):
|
||||
os.unlink(temporary)
|
||||
raise
|
||||
Reference in New Issue
Block a user