feat(lidar): add RAVNOVES field review
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.compute import (
|
||||
PATCHWORKPP_SOURCE_COMMIT,
|
||||
PATCHWORKPP_SOURCE_TAG,
|
||||
RAVNOVES00_CENTRAL_WINDOWS,
|
||||
E10LidarFieldSource,
|
||||
GroundBenchmarkProfile,
|
||||
LidarFieldReviewV1,
|
||||
PatchworkPPGroundSegmenter,
|
||||
build_lidar_field_review,
|
||||
)
|
||||
|
||||
|
||||
def _arguments() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Build accumulated RAVNOVES00 central-urban LiDAR ground review "
|
||||
"with content-bound camera context previews."
|
||||
)
|
||||
)
|
||||
parser.add_argument("source_pack", type=Path)
|
||||
parser.add_argument("camera_epoch", type=Path)
|
||||
parser.add_argument("output_root", type=Path)
|
||||
parser.add_argument("--ffmpeg", default="ffmpeg")
|
||||
parser.add_argument("--patchwork-module", default="pypatchworkpp")
|
||||
parser.add_argument("--patchwork-source-tag", default=PATCHWORKPP_SOURCE_TAG)
|
||||
parser.add_argument(
|
||||
"--patchwork-source-commit",
|
||||
default=PATCHWORKPP_SOURCE_COMMIT,
|
||||
)
|
||||
parser.add_argument("--sensor-height-m", type=float, default=1.27)
|
||||
parser.add_argument("--map-vertical-origin-offset-m", type=float, default=1.27)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def _preview_segments(epoch: Path) -> dict[str, Path]:
|
||||
resolved = epoch.expanduser().resolve(strict=True)
|
||||
init = resolved / "init.mp4"
|
||||
index_path = resolved / "index.jsonl"
|
||||
if not init.is_file() or init.is_symlink() or not index_path.is_file():
|
||||
raise RuntimeError("Camera epoch is incomplete")
|
||||
required = {
|
||||
window.preview_source_frame_index + 1: window.key for window in RAVNOVES00_CENTRAL_WINDOWS
|
||||
}
|
||||
found: dict[str, Path] = {}
|
||||
with index_path.open(encoding="utf-8") as stream:
|
||||
for line in stream:
|
||||
value = json.loads(line)
|
||||
sequence = value.get("sequence") if isinstance(value, dict) else None
|
||||
if sequence not in required:
|
||||
continue
|
||||
relative = value.get("path")
|
||||
expected_sha256 = value.get("sha256")
|
||||
expected_length = value.get("length")
|
||||
if (
|
||||
value.get("schema_version") != "missioncore.camera-recording-index/v1"
|
||||
or relative != f"segments/{sequence}.m4s"
|
||||
or not isinstance(expected_sha256, str)
|
||||
or not isinstance(expected_length, int)
|
||||
):
|
||||
raise RuntimeError("Camera preview segment index is invalid")
|
||||
segment = resolved / relative
|
||||
if (
|
||||
segment.is_symlink()
|
||||
or not segment.is_file()
|
||||
or segment.stat().st_size != expected_length
|
||||
or _sha256(segment) != expected_sha256
|
||||
):
|
||||
raise RuntimeError("Camera preview segment failed integrity")
|
||||
found[required[sequence]] = segment
|
||||
if set(found) != set(required.values()):
|
||||
raise RuntimeError("Camera preview segments are incomplete")
|
||||
return found
|
||||
|
||||
|
||||
def _extract_previews(
|
||||
ffmpeg: str,
|
||||
epoch: Path,
|
||||
output: Path,
|
||||
) -> dict[str, Path]:
|
||||
init = epoch.expanduser().resolve(strict=True) / "init.mp4"
|
||||
segments = _preview_segments(epoch)
|
||||
previews: dict[str, Path] = {}
|
||||
for key, segment in segments.items():
|
||||
target = output / f"{key}.jpg"
|
||||
subprocess.run(
|
||||
[
|
||||
ffmpeg,
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-i",
|
||||
f"concat:{init}|{segment}",
|
||||
"-frames:v",
|
||||
"1",
|
||||
"-vf",
|
||||
"scale=640:480",
|
||||
"-q:v",
|
||||
"3",
|
||||
str(target),
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
if not target.is_file() or target.stat().st_size < 1_000:
|
||||
raise RuntimeError("Camera preview extraction failed")
|
||||
previews[key] = target
|
||||
return previews
|
||||
|
||||
|
||||
def main() -> int:
|
||||
arguments = _arguments()
|
||||
profile = GroundBenchmarkProfile(
|
||||
profile_id="ravnoves00-central-urban-operator-height-ground-review/v1",
|
||||
patchwork_sensor_height_proxy_m=arguments.sensor_height_m,
|
||||
patchwork_map_vertical_origin_offset_m=(arguments.map_vertical_origin_offset_m),
|
||||
patchwork_height_evidence="operator-estimated",
|
||||
)
|
||||
source = E10LidarFieldSource(arguments.source_pack)
|
||||
try:
|
||||
patchwork = PatchworkPPGroundSegmenter.load(
|
||||
profile=profile,
|
||||
module_name=arguments.patchwork_module,
|
||||
source_tag=arguments.patchwork_source_tag,
|
||||
source_commit=arguments.patchwork_source_commit,
|
||||
)
|
||||
with tempfile.TemporaryDirectory(prefix="missioncore-lidar-field-review-") as value:
|
||||
previews = _extract_previews(
|
||||
arguments.ffmpeg,
|
||||
arguments.camera_epoch,
|
||||
Path(value),
|
||||
)
|
||||
output = build_lidar_field_review(
|
||||
source,
|
||||
arguments.output_root,
|
||||
patchwork=patchwork,
|
||||
profile=profile,
|
||||
preview_paths=previews,
|
||||
)
|
||||
finally:
|
||||
source.close()
|
||||
review = LidarFieldReviewV1(output)
|
||||
try:
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"review_id": review.review_id,
|
||||
"display_name": review.report["display_name"],
|
||||
"session_id": review.report["session_id"],
|
||||
"source": review.report["source"],
|
||||
"selection": review.report["selection"],
|
||||
"windows": review.report["windows"],
|
||||
"metrics": review.report["metrics"],
|
||||
"decision": review.report["decision"],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
finally:
|
||||
review.close()
|
||||
return 0
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while chunk := stream.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user