单目3D初始代码

This commit is contained in:
zhao.zhu
2026-06-24 09:35:46 +08:00
commit 04a5895b6b
1153 changed files with 340700 additions and 0 deletions

View File

@@ -0,0 +1 @@
"""Input adapters for model_inference batch sources."""

View File

@@ -0,0 +1,822 @@
from __future__ import annotations
import argparse
from concurrent.futures import ThreadPoolExecutor, as_completed
import json
from dataclasses import dataclass
from pathlib import Path
import re
from typing import Any, Callable, Optional
try:
from .get_clip_by_eventid import get_associated_clip_ids
from .pdcl_clip_export_utils import (
build_clip_tasks_from_clip_ids,
run_clip_tasks_inference_exported,
)
except ImportError:
from get_clip_by_eventid import get_associated_clip_ids
from pdcl_clip_export_utils import (
build_clip_tasks_from_clip_ids,
run_clip_tasks_inference_exported,
)
DEFAULT_EVENT_CACHE_FILE = Path(__file__).resolve().parents[1] / ".cache" / "event_clip_cache.json"
@dataclass(frozen=True)
class ResolvedEventRecord:
scene: str
record_index: int
event_id: str
event_id_field_used: str
source_record: dict[str, Any]
clip_ids: list[str]
clip_source: str
@dataclass(frozen=True)
class EventResolutionStats:
total_events: int
cache_hits: int
cache_misses: int
request_workers: int
cache_file: str
direct_clip_records: int = 0
event_lookup_records: int = 0
def _dedupe_preserve_order(values: list[str]) -> list[str]:
ordered: list[str] = []
seen: set[str] = set()
for value in values:
token = str(value).strip()
if not token or token in seen:
continue
seen.add(token)
ordered.append(token)
return ordered
def _sanitize_identifier_for_path(identifier: str, prefix: str = "event_id") -> str:
token = re.sub(r'[\\/:*?"<>|\s]+', "_", str(identifier or "").strip())
token = token.strip("._")
token = token or "unknown_id"
normalized_prefix = re.sub(r"[^0-9A-Za-z]+", "_", str(prefix or "").strip())
normalized_prefix = normalized_prefix.strip("._") or "id"
return f"{normalized_prefix}_{token}"
def _build_resolution_stats_payload(resolution_stats: "EventResolutionStats") -> dict[str, Any]:
return {
"total_events": resolution_stats.total_events,
"cache_hits": resolution_stats.cache_hits,
"cache_misses": resolution_stats.cache_misses,
"request_workers": resolution_stats.request_workers,
"cache_file": resolution_stats.cache_file,
"direct_clip_records": resolution_stats.direct_clip_records,
"event_lookup_records": resolution_stats.event_lookup_records,
}
def _record_key(record: "ResolvedEventRecord") -> tuple[str, int, str]:
return (record.scene, int(record.record_index), str(record.event_id))
def _normalize_direct_clip_ids(raw_value: Any) -> list[str]:
if isinstance(raw_value, list):
return _dedupe_preserve_order([str(item).strip() for item in raw_value if str(item).strip()])
if isinstance(raw_value, str):
text = raw_value.strip()
if not text:
return []
if text.startswith("["):
try:
parsed = json.loads(text)
except Exception:
parsed = None
if isinstance(parsed, list):
return _dedupe_preserve_order([str(item).strip() for item in parsed if str(item).strip()])
return _dedupe_preserve_order([token for token in re.split(r"[\s,]+", text) if token])
if raw_value is None:
return []
token = str(raw_value).strip()
if not token:
return []
return [token]
def _extract_direct_clip_ids(record: dict[str, Any], clip_ids_field: str) -> tuple[bool, list[str]]:
field_name = str(clip_ids_field or "").strip()
if not field_name or field_name not in record:
return False, []
return True, _normalize_direct_clip_ids(record.get(field_name))
def _resolve_record_identifier(
record: dict[str, Any],
preferred_field: str,
*,
allow_direct_clip_fallback: bool = False,
) -> tuple[str, str]:
candidate_fields: list[str] = []
preferred_token = str(preferred_field or "").strip()
if preferred_token:
candidate_fields.append(preferred_token)
if allow_direct_clip_fallback:
for field_name in ("rawid", "event_id", "data_path"):
if field_name not in candidate_fields:
candidate_fields.append(field_name)
for field_name in candidate_fields:
identifier = str(record.get(field_name, "")).strip()
if identifier:
return identifier, field_name
return "", preferred_token
def _extract_condition_values(source_record: dict[str, Any], condition_fields: list[str]) -> dict[str, str]:
return {
str(field): str(source_record.get(field, "")).strip()
for field in condition_fields
}
def _select_event_records_by_condition(
records: list["ResolvedEventRecord"],
*,
condition_fields: list[str],
max_records_per_condition: int,
selection_strategy: str,
) -> tuple[list["ResolvedEventRecord"], dict[str, Any]]:
normalized_fields = [str(field).strip() for field in condition_fields if str(field).strip()]
limit = max(0, int(max_records_per_condition))
selection_enabled = bool(normalized_fields and limit > 0)
summary: dict[str, Any] = {
"enabled": selection_enabled,
"condition_fields": normalized_fields,
"max_records_per_condition": limit,
"selection_strategy": selection_strategy,
"records_before_selection": len(records),
"records_after_selection": len(records),
"group_count": 0,
"groups": [],
}
if not selection_enabled:
return list(records), summary
if selection_strategy != "first":
raise ValueError(f"Unsupported condition selection strategy: {selection_strategy!r}")
selected_records: list[ResolvedEventRecord] = []
selected_counts: dict[tuple[str, tuple[str, ...]], int] = {}
group_summaries: dict[tuple[str, tuple[str, ...]], dict[str, Any]] = {}
for record in records:
condition_values = _extract_condition_values(record.source_record, normalized_fields)
condition_tuple = tuple(condition_values[field] for field in normalized_fields)
group_key = (record.scene, condition_tuple)
group_summary = group_summaries.setdefault(
group_key,
{
"scene": record.scene,
"condition_values": condition_values,
"record_count": 0,
"selected_count": 0,
"skipped_count": 0,
"selected_record_ids": [],
"selected_record_indices": [],
"skipped_record_ids": [],
"skipped_record_indices": [],
},
)
group_summary["record_count"] += 1
current_selected_count = selected_counts.get(group_key, 0)
if current_selected_count < limit:
selected_records.append(record)
selected_counts[group_key] = current_selected_count + 1
group_summary["selected_count"] += 1
group_summary["selected_record_ids"].append(record.event_id)
group_summary["selected_record_indices"].append(record.record_index)
continue
group_summary["skipped_count"] += 1
group_summary["skipped_record_ids"].append(record.event_id)
group_summary["skipped_record_indices"].append(record.record_index)
summary["records_after_selection"] = len(selected_records)
summary["group_count"] = len(group_summaries)
summary["groups"] = list(group_summaries.values())
return selected_records, summary
def _filter_selection_summary_for_scene(selection_summary: dict[str, Any], scene: str) -> dict[str, Any]:
if not selection_summary:
return {}
scene_groups = [
dict(group)
for group in selection_summary.get("groups", [])
if str(group.get("scene", "")) == str(scene)
]
filtered = dict(selection_summary)
filtered["groups"] = scene_groups
filtered["group_count"] = len(scene_groups)
if scene_groups:
filtered["records_before_selection"] = sum(int(group.get("record_count", 0)) for group in scene_groups)
filtered["records_after_selection"] = sum(int(group.get("selected_count", 0)) for group in scene_groups)
else:
filtered["records_before_selection"] = 0
filtered["records_after_selection"] = 0
return filtered
def _format_condition_summary(source_record: dict[str, Any], condition_fields: list[str]) -> str:
normalized_fields = [str(field).strip() for field in condition_fields if str(field).strip()]
if not normalized_fields:
return ""
values = _extract_condition_values(source_record, normalized_fields)
return ", ".join(f"{field}={values.get(field, '')}" for field in normalized_fields)
def _print_event_json_summary(
*,
args: argparse.Namespace,
manifest_path: Path,
resolved_records: list["ResolvedEventRecord"],
selected_records: list["ResolvedEventRecord"],
selection_summary: dict[str, Any],
scene_results: dict[str, dict[str, Any]],
) -> None:
print(f"Event JSON file: {args.event_json_file}")
if args.scene:
print(f"Scene filter: {args.scene}")
print(
"Resolved records: "
f"{len(resolved_records)}, selected records: {len(selected_records)}"
)
print(f"Manifest: {manifest_path}")
if not selected_records:
print("No records selected.")
return
condition_fields = [str(field).strip() for field in getattr(args, "condition_fields", []) if str(field).strip()]
if args.selection_only:
print("Selection-only mode: no clip export or inference was run.")
elif selection_summary.get("enabled"):
print(
"Condition selection: "
f"{selection_summary.get('group_count', 0)} groups, "
f"strategy={selection_summary.get('selection_strategy', '')}, "
f"max_records_per_condition={selection_summary.get('max_records_per_condition', 0)}"
)
for scene in sorted(scene_results):
scene_result = scene_results[scene]
print(
f"Scene {scene}: selected_record_count="
f"{scene_result.get('selected_record_count', len([record for record in selected_records if record.scene == scene]))}"
)
scene_manifest_path = scene_result.get("scene_manifest_path")
if scene_manifest_path:
print(f" Scene manifest: {scene_manifest_path}")
if scene_result.get("scene_output_dir"):
print(f" Output dir: {scene_result['scene_output_dir']}")
scene_records = [record for record in selected_records if record.scene == scene]
for record in scene_records:
condition_summary = _format_condition_summary(record.source_record, condition_fields)
summary_parts = []
if condition_summary:
summary_parts.append(condition_summary)
summary_parts.append(f"record_id={record.event_id}")
summary_parts.append(f"clips={len(record.clip_ids)}")
print(f" - {'; '.join(summary_parts)}")
def _build_scene_manifest_payload(
*,
args: argparse.Namespace,
scene: str,
scene_records: list["ResolvedEventRecord"],
resolution_stats: "EventResolutionStats",
selection_summary: Optional[dict[str, Any]] = None,
) -> dict[str, Any]:
payload = {
"event_json_file": args.event_json_file,
"scene": scene,
"event_id_field": args.event_id_field,
"event_clip_ids_field": args.event_clip_ids_field,
"resolution_stats": _build_resolution_stats_payload(resolution_stats),
"event_record_count": len(scene_records),
"records": [
{
"record_index": record.record_index,
"event_id": record.event_id,
"event_id_field_used": record.event_id_field_used,
"clip_ids": record.clip_ids,
"clip_source": record.clip_source,
"source_record": record.source_record,
}
for record in scene_records
],
}
if selection_summary is not None:
payload["selection"] = selection_summary
return payload
def _build_event_manifest_payload(
*,
args: argparse.Namespace,
scene: str,
event_id: str,
event_records: list["ResolvedEventRecord"],
clip_ids: list[str],
resolution_stats: "EventResolutionStats",
) -> dict[str, Any]:
condition_fields = [str(field).strip() for field in getattr(args, "condition_fields", []) if str(field).strip()]
payload = {
"event_json_file": args.event_json_file,
"scene": scene,
"event_id_field": args.event_id_field,
"event_clip_ids_field": args.event_clip_ids_field,
"event_id": event_id,
"resolution_stats": _build_resolution_stats_payload(resolution_stats),
"event_record_count": len(event_records),
"clip_ids": clip_ids,
"clip_count": len(clip_ids),
"records": [
{
"record_index": record.record_index,
"event_id": record.event_id,
"event_id_field_used": record.event_id_field_used,
"clip_ids": record.clip_ids,
"clip_source": record.clip_source,
"source_record": record.source_record,
}
for record in event_records
],
}
if event_records:
payload["event_id_field_used"] = event_records[0].event_id_field_used
payload["clip_source"] = sorted({record.clip_source for record in event_records})
if condition_fields and event_records:
payload["condition_values"] = _extract_condition_values(event_records[0].source_record, condition_fields)
return payload
def load_event_scene_json(json_file: str) -> dict[str, list[dict[str, Any]]]:
path = Path(json_file)
with path.open("r", encoding="utf-8") as file:
payload = json.load(file)
if not isinstance(payload, dict):
raise ValueError(f"Expected top-level dict in {path}, got {type(payload).__name__}")
normalized: dict[str, list[dict[str, Any]]] = {}
for scene, records in payload.items():
if not isinstance(records, list):
raise ValueError(f"Scene {scene!r} should map to a list, got {type(records).__name__}")
normalized[str(scene)] = [dict(item) if isinstance(item, dict) else {"value": item} for item in records]
return normalized
def load_event_clip_cache(cache_file: str | Path) -> dict[str, list[str]]:
cache_path = Path(cache_file)
if not cache_path.exists():
return {}
try:
payload = json.loads(cache_path.read_text(encoding="utf-8"))
except Exception:
return {}
if not isinstance(payload, dict):
return {}
cache: dict[str, list[str]] = {}
for event_id, clip_ids in payload.items():
if isinstance(clip_ids, list):
cache[str(event_id)] = [str(item).strip() for item in clip_ids if str(item).strip()]
return cache
def save_event_clip_cache(cache_file: str | Path, cache_payload: dict[str, list[str]]) -> Path:
cache_path = Path(cache_file)
cache_path.parent.mkdir(parents=True, exist_ok=True)
normalized = {
str(event_id): [str(item).strip() for item in clip_ids if str(item).strip()]
for event_id, clip_ids in cache_payload.items()
}
with cache_path.open("w", encoding="utf-8") as file:
json.dump(normalized, file, indent=2, ensure_ascii=False)
return cache_path
def resolve_event_clip_ids(
event_ids: list[str],
*,
timeout: float = 60.0,
cache_file: str | Path = DEFAULT_EVENT_CACHE_FILE,
workers: int = 4,
max_retries: int = 3,
retry_backoff_sec: float = 2.0,
) -> tuple[dict[str, list[str]], EventResolutionStats]:
ordered_event_ids: list[str] = []
seen: set[str] = set()
for event_id in event_ids:
normalized = str(event_id).strip()
if not normalized or normalized in seen:
continue
seen.add(normalized)
ordered_event_ids.append(normalized)
cache_payload = load_event_clip_cache(cache_file)
resolved: dict[str, list[str]] = {}
unresolved_event_ids: list[str] = []
cache_hits = 0
cache_misses = 0
for event_id in ordered_event_ids:
if event_id in cache_payload:
resolved[event_id] = list(cache_payload[event_id])
cache_hits += 1
else:
unresolved_event_ids.append(event_id)
cache_misses += 1
if unresolved_event_ids:
max_workers = max(1, min(int(workers), len(unresolved_event_ids)))
if max_workers == 1:
for event_id in unresolved_event_ids:
clip_ids = get_associated_clip_ids(
event_id,
timeout=timeout,
max_retries=max_retries,
retry_backoff_sec=retry_backoff_sec,
)
resolved[event_id] = clip_ids
cache_payload[event_id] = clip_ids
else:
with ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_event_id = {
executor.submit(
get_associated_clip_ids,
event_id,
timeout,
max_retries,
retry_backoff_sec,
): event_id
for event_id in unresolved_event_ids
}
for future in as_completed(future_to_event_id):
event_id = future_to_event_id[future]
clip_ids = future.result()
resolved[event_id] = clip_ids
cache_payload[event_id] = clip_ids
save_event_clip_cache(cache_file, cache_payload)
worker_count = max(1, min(int(workers), len(unresolved_event_ids)))
else:
worker_count = 0
stats = EventResolutionStats(
total_events=len(ordered_event_ids),
cache_hits=cache_hits,
cache_misses=cache_misses,
request_workers=worker_count,
cache_file=str(Path(cache_file).resolve()),
)
return resolved, stats
def resolve_event_records(
json_file: str,
scene_filter: Optional[str] = None,
event_id_field: str = "data_path",
clip_ids_field: str = "clips",
max_events: int = 0,
timeout: float = 60.0,
cache_file: str | Path = DEFAULT_EVENT_CACHE_FILE,
workers: int = 4,
max_retries: int = 3,
retry_backoff_sec: float = 2.0,
) -> tuple[list[ResolvedEventRecord], EventResolutionStats]:
scene_payload = load_event_scene_json(json_file)
scene_names = [scene_filter] if scene_filter else list(scene_payload)
pending_records: list[tuple[str, int, dict[str, Any], str, str, list[str], str]] = []
lookup_event_ids: list[str] = []
processed = 0
direct_clip_records = 0
event_lookup_records = 0
for scene in scene_names:
records = scene_payload.get(scene)
if records is None:
raise ValueError(f"Scene {scene!r} not found in {json_file}")
for index, record in enumerate(records):
has_direct_clip_ids, direct_clip_ids = _extract_direct_clip_ids(record, clip_ids_field)
event_id, resolved_id_field = _resolve_record_identifier(
record,
event_id_field,
allow_direct_clip_fallback=has_direct_clip_ids,
)
if not event_id:
continue
clip_source = "direct_clip_ids_field" if has_direct_clip_ids else "event_lookup"
if has_direct_clip_ids:
direct_clip_records += 1
else:
event_lookup_records += 1
lookup_event_ids.append(event_id)
pending_records.append(
(scene, index, record, event_id, resolved_id_field, direct_clip_ids, clip_source)
)
processed += 1
if max_events > 0 and processed >= max_events:
break
if max_events > 0 and processed >= max_events:
break
resolved_clip_map: dict[str, list[str]] = {}
lookup_stats = EventResolutionStats(
total_events=0,
cache_hits=0,
cache_misses=0,
request_workers=0,
cache_file=str(Path(cache_file).resolve()),
)
if lookup_event_ids:
resolved_clip_map, lookup_stats = resolve_event_clip_ids(
lookup_event_ids,
timeout=timeout,
cache_file=cache_file,
workers=workers,
max_retries=max_retries,
retry_backoff_sec=retry_backoff_sec,
)
resolved = [
ResolvedEventRecord(
scene=scene,
record_index=index,
event_id=event_id,
event_id_field_used=resolved_id_field,
source_record=record,
clip_ids=direct_clip_ids if clip_source == "direct_clip_ids_field" else resolved_clip_map.get(event_id, []),
clip_source=clip_source,
)
for scene, index, record, event_id, resolved_id_field, direct_clip_ids, clip_source in pending_records
]
stats = EventResolutionStats(
total_events=len(pending_records),
cache_hits=lookup_stats.cache_hits,
cache_misses=lookup_stats.cache_misses,
request_workers=lookup_stats.request_workers,
cache_file=str(Path(cache_file).resolve()),
direct_clip_records=direct_clip_records,
event_lookup_records=event_lookup_records,
)
return resolved, stats
def build_event_resolution_payload(
json_file: str,
resolved_records: list[ResolvedEventRecord],
event_id_field: str,
clip_ids_field: str,
stats: EventResolutionStats,
selection_summary: Optional[dict[str, Any]] = None,
selected_record_keys: Optional[set[tuple[str, int, str]]] = None,
) -> dict[str, Any]:
selection_enabled = bool(selection_summary and selection_summary.get("enabled"))
selected_keys = selected_record_keys or set()
scenes: dict[str, list[dict[str, Any]]] = {}
for record in resolved_records:
scenes.setdefault(record.scene, []).append(
{
"record_index": record.record_index,
"event_id_field": event_id_field,
"event_id_field_used": record.event_id_field_used,
"event_id": record.event_id,
"event_clip_ids_field": clip_ids_field,
"clip_ids": record.clip_ids,
"clip_count": len(record.clip_ids),
"clip_source": record.clip_source,
"selected_for_inference": True if not selection_enabled else _record_key(record) in selected_keys,
"source_record": record.source_record,
}
)
payload = {
"event_json_file": json_file,
"event_id_field": event_id_field,
"event_clip_ids_field": clip_ids_field,
"scene_count": len(scenes),
"record_count": len(resolved_records),
"selected_record_count": len(selected_keys) if selection_enabled else len(resolved_records),
"resolution_stats": _build_resolution_stats_payload(stats),
"scenes": scenes,
}
if selection_summary is not None:
payload["selection"] = selection_summary
return payload
def save_event_resolution_manifest(
output_root: Path,
json_file: str,
resolved_records: list[ResolvedEventRecord],
event_id_field: str,
clip_ids_field: str,
stats: EventResolutionStats,
selection_summary: Optional[dict[str, Any]] = None,
selected_record_keys: Optional[set[tuple[str, int, str]]] = None,
) -> Path:
manifest_path = output_root / "_status" / "event_scene_manifest.json"
manifest_path.parent.mkdir(parents=True, exist_ok=True)
payload = build_event_resolution_payload(
json_file,
resolved_records,
event_id_field,
clip_ids_field,
stats,
selection_summary=selection_summary,
selected_record_keys=selected_record_keys,
)
with manifest_path.open("w", encoding="utf-8") as file:
json.dump(payload, file, indent=2, ensure_ascii=False)
return manifest_path
def run_event_json_inference_exported(
*,
context: Any,
args: argparse.Namespace,
load_env: Callable[[], Any],
run_case_inference: Callable[..., dict[str, Any]],
) -> dict[str, Any]:
resolved_records, resolution_stats = resolve_event_records(
json_file=args.event_json_file,
scene_filter=args.scene,
event_id_field=args.event_id_field,
clip_ids_field=args.event_clip_ids_field,
max_events=args.max_events,
timeout=args.event_request_timeout,
cache_file=args.event_cache_file,
workers=args.event_resolve_workers,
max_retries=args.event_request_retries,
retry_backoff_sec=args.event_request_retry_backoff_sec,
)
selected_records, selection_summary = _select_event_records_by_condition(
resolved_records,
condition_fields=[str(field).strip() for field in getattr(args, "condition_fields", []) if str(field).strip()],
max_records_per_condition=int(getattr(args, "max_records_per_condition", 0)),
selection_strategy=str(getattr(args, "condition_select_strategy", "first")),
)
selected_record_keys = {_record_key(record) for record in selected_records}
output_root = Path(args.output_dir).resolve()
output_root.mkdir(parents=True, exist_ok=True)
manifest_path = save_event_resolution_manifest(
output_root=output_root,
json_file=args.event_json_file,
resolved_records=resolved_records,
event_id_field=args.event_id_field,
clip_ids_field=args.event_clip_ids_field,
stats=resolution_stats,
selection_summary=selection_summary,
selected_record_keys=selected_record_keys,
)
summary_by_scene: dict[str, dict[str, Any]] = {}
for scene in sorted({record.scene for record in selected_records}):
scene_records = [record for record in selected_records if record.scene == scene]
scene_selection_summary = _filter_selection_summary_for_scene(selection_summary, scene)
scene_export_root = Path(args.export_root).resolve() / scene
scene_output_root = output_root / scene
scene_output_root.mkdir(parents=True, exist_ok=True)
scene_manifest_path = scene_output_root / "_status" / "scene_event_manifest.json"
scene_manifest_path.parent.mkdir(parents=True, exist_ok=True)
with scene_manifest_path.open("w", encoding="utf-8") as file:
json.dump(
_build_scene_manifest_payload(
args=args,
scene=scene,
scene_records=scene_records,
resolution_stats=resolution_stats,
selection_summary=scene_selection_summary,
),
file,
indent=2,
ensure_ascii=False,
)
if args.selection_only:
summary_by_scene[scene] = {
"scene_output_dir": str(scene_output_root),
"scene_manifest_path": str(scene_manifest_path),
"selected_record_count": len(scene_records),
"selection_only": True,
}
continue
event_records_by_id: dict[str, list[ResolvedEventRecord]] = {}
event_order: list[str] = []
for record in scene_records:
if record.event_id not in event_records_by_id:
event_order.append(record.event_id)
event_records_by_id.setdefault(record.event_id, []).append(record)
selected_scene_clip_ids: set[str] = set()
event_results: dict[str, dict[str, Any]] = {}
event_dirs: dict[str, str] = {}
event_export_dirs: dict[str, str] = {}
for event_id in event_order:
event_records = event_records_by_id[event_id]
event_clip_ids = _dedupe_preserve_order(
[clip_id for record in event_records for clip_id in record.clip_ids]
)
if args.limit_clips > 0:
limited_clip_ids: list[str] = []
for clip_id in event_clip_ids:
if clip_id in selected_scene_clip_ids:
limited_clip_ids.append(clip_id)
continue
if len(selected_scene_clip_ids) >= args.limit_clips:
continue
selected_scene_clip_ids.add(clip_id)
limited_clip_ids.append(clip_id)
event_clip_ids = limited_clip_ids
clip_tasks = build_clip_tasks_from_clip_ids(event_clip_ids)
dir_prefix = "event_id"
if any(record.clip_source == "direct_clip_ids_field" for record in event_records):
dir_prefix = event_records[0].event_id_field_used or args.event_id_field or "record_id"
event_dir_name = _sanitize_identifier_for_path(event_id, prefix=dir_prefix)
event_export_root = scene_export_root / event_dir_name
event_output_root = scene_output_root / event_dir_name
event_export_dirs[event_id] = str(event_export_root)
event_dirs[event_id] = str(event_output_root)
def on_before_run(export_root: Path, inference_root: Path, tasks: list[Any], *, _event_id: str = event_id, _event_records: list[ResolvedEventRecord] = event_records, _event_clip_ids: list[str] = event_clip_ids) -> None:
payload = _build_event_manifest_payload(
args=args,
scene=scene,
event_id=_event_id,
event_records=_event_records,
clip_ids=_event_clip_ids,
resolution_stats=resolution_stats,
)
manifest_path = inference_root / "_status" / "event_manifest.json"
manifest_path.parent.mkdir(parents=True, exist_ok=True)
with manifest_path.open("w", encoding="utf-8") as file:
json.dump(payload, file, indent=2, ensure_ascii=False)
event_results[event_id] = run_clip_tasks_inference_exported(
context=context,
args=args,
clip_tasks=clip_tasks,
load_env=load_env,
run_case_inference=run_case_inference,
export_root=event_export_root,
output_root=event_output_root,
on_before_run=on_before_run,
)
summary_by_scene[scene] = {
"scene_output_dir": str(scene_output_root),
"scene_manifest_path": str(scene_manifest_path),
"event_export_dirs": event_export_dirs,
"event_dirs": event_dirs,
"event_results": event_results,
}
result = {
"event_json_file": args.event_json_file,
"scene": args.scene or "",
"event_id_field": args.event_id_field,
"event_clip_ids_field": args.event_clip_ids_field,
"manifest_path": str(manifest_path),
"selection": selection_summary,
"selected_record_count": len(selected_records),
"resolution_stats": _build_resolution_stats_payload(resolution_stats),
"scene_results": summary_by_scene,
}
_print_event_json_summary(
args=args,
manifest_path=manifest_path,
resolved_records=resolved_records,
selected_records=selected_records,
selection_summary=selection_summary,
scene_results=summary_by_scene,
)
return result

View File

@@ -0,0 +1,141 @@
from __future__ import annotations
import argparse
import json
import time
from typing import Any
import requests
EVENT_META_INFO_URL = "https://d.minieye.tech/ess-api/dashoard/event/meta_info"
CLIP_ID_KEYS = ("clip_id", "clip_uuid", "clip_ukey", "ukey", "id")
def fetch_event_meta(
event_id: str,
timeout: float = 60.0,
max_retries: int = 3,
retry_backoff_sec: float = 2.0,
) -> dict[str, Any]:
event_id = str(event_id).strip()
if not event_id:
raise ValueError("event_id is required")
last_error: Exception | None = None
total_attempts = max(1, int(max_retries))
for attempt in range(1, total_attempts + 1):
try:
response = requests.post(
url=EVENT_META_INFO_URL,
json={"event_id": event_id},
timeout=timeout,
)
response.raise_for_status()
payload = response.json()
if not isinstance(payload, dict):
raise ValueError(f"Unexpected event meta response type: {type(payload).__name__}")
return payload
except (requests.exceptions.Timeout, requests.exceptions.RequestException, ValueError) as exc:
last_error = exc
if attempt >= total_attempts:
break
sleep_sec = max(0.0, float(retry_backoff_sec)) * (2 ** (attempt - 1))
print(
f"[warn] event_id={event_id} request attempt {attempt}/{total_attempts} failed: "
f"{type(exc).__name__}: {exc}. retry in {sleep_sec:.1f}s"
)
if sleep_sec > 0:
time.sleep(sleep_sec)
assert last_error is not None
raise last_error
def _append_clip_id(clip_ids: list[str], seen: set[str], value: Any) -> None:
clip_id = str(value).strip()
if clip_id and clip_id not in seen:
seen.add(clip_id)
clip_ids.append(clip_id)
def _extract_clip_ids_from_item(item: Any, clip_ids: list[str], seen: set[str]) -> None:
if item is None:
return
if isinstance(item, str):
_append_clip_id(clip_ids, seen, item)
return
if isinstance(item, (list, tuple, set)):
for sub_item in item:
_extract_clip_ids_from_item(sub_item, clip_ids, seen)
return
if isinstance(item, dict):
matched_key = False
for key in CLIP_ID_KEYS:
value = item.get(key)
if value is not None:
matched_key = True
_extract_clip_ids_from_item(value, clip_ids, seen)
if matched_key:
return
for value in item.values():
_extract_clip_ids_from_item(value, clip_ids, seen)
return
_append_clip_id(clip_ids, seen, item)
def extract_associated_clip_ids(payload: dict[str, Any]) -> list[str]:
clip_items = payload.get("associated_clip_list", [])
if clip_items is None:
return []
clip_ids: list[str] = []
seen: set[str] = set()
_extract_clip_ids_from_item(clip_items, clip_ids, seen)
return clip_ids
def get_associated_clip_ids(
event_id: str,
timeout: float = 60.0,
max_retries: int = 3,
retry_backoff_sec: float = 2.0,
) -> list[str]:
payload = fetch_event_meta(
event_id,
timeout=timeout,
max_retries=max_retries,
retry_backoff_sec=retry_backoff_sec,
)
return extract_associated_clip_ids(payload)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Resolve PDCL clip ids from one or more event ids.")
parser.add_argument("event_ids", nargs="+", help="One or more event ids to resolve.")
parser.add_argument("--timeout", type=float, default=60.0, help="HTTP request timeout in seconds.")
parser.add_argument("--retries", type=int, default=3, help="Max request attempts per event id.")
parser.add_argument("--retry-backoff-sec", type=float, default=2.0, help="Base retry backoff in seconds.")
parser.add_argument("--pretty", action="store_true", help="Pretty-print the JSON result.")
return parser.parse_args()
def main() -> None:
args = parse_args()
result = {
event_id: get_associated_clip_ids(
event_id,
timeout=args.timeout,
max_retries=args.retries,
retry_backoff_sec=args.retry_backoff_sec,
)
for event_id in args.event_ids
}
if args.pretty:
print(json.dumps(result, indent=2, ensure_ascii=False))
else:
print(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,826 @@
from __future__ import annotations
import argparse
import io
import json
import os
import traceback
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from pathlib import Path
from threading import Lock
from typing import Any, Callable, Optional
import cv2
import numpy as np
CALIB_ATTACHMENT_NAMES = (
"sigmastar.1/calibs/camera4.json",
"test_data/calibs/camera4.json",
"calibs/camera4.json",
)
BEIJING_TZ = timezone(timedelta(hours=8))
DEFAULT_DATE_NAME = "unknown_date"
DEFAULT_VEHICLE_NAME = "unknown_vehicle"
LOCAL_MCAP_VEHICLE_NAME = "local_mcap"
@dataclass(frozen=True)
class ClipTask:
clip_uuid: str
date_name: str
vehicle_name: str
clip_path: str
@property
def task_id(self) -> str:
return self.clip_uuid
@dataclass
class TaskResult:
task_id: str
success: bool
message: str
output_dir: Optional[str] = None
class StatusStore:
"""Persistent status tracker for resumable batch inference."""
def __init__(self, status_file: Path):
self.status_file = status_file
self.lock = Lock()
self._status: dict[str, dict[str, str]] = {}
self.status_file.parent.mkdir(parents=True, exist_ok=True)
if self.status_file.exists():
try:
self._status = json.loads(self.status_file.read_text())
except Exception:
self._status = {}
def is_done(self, task_id: str) -> bool:
return self._status.get(task_id, {}).get("state") == "done"
def get(self, task_id: str) -> Optional[dict[str, str]]:
return self._status.get(task_id)
def mark_running(self, task_id: str, detail: str) -> None:
with self.lock:
self._status[task_id] = {"state": "running", "detail": detail}
self._flush()
def mark_done(self, task_id: str, detail: str) -> None:
with self.lock:
self._status[task_id] = {"state": "done", "detail": detail}
self._flush()
def mark_failed(self, task_id: str, detail: str) -> None:
with self.lock:
self._status[task_id] = {"state": "failed", "detail": detail}
self._flush()
def summary(self) -> dict[str, int]:
done = 0
running = 0
failed = 0
for item in self._status.values():
state = item.get("state")
if state == "done":
done += 1
elif state == "running":
running += 1
elif state == "failed":
failed += 1
return {"done": done, "running": running, "failed": failed, "total": len(self._status)}
def _flush(self) -> None:
self.status_file.write_text(json.dumps(self._status, indent=2, ensure_ascii=False))
def _ensure_pdcl_auth_defaults() -> None:
os.environ.setdefault("STS_UID", "dis-uploader")
os.environ.setdefault("STS_SECRET_KEY", "277310cc09724d315514a79701fecb0f")
def validate_pdcl_auth_env() -> None:
_ensure_pdcl_auth_defaults()
required_vars = ["STS_UID", "STS_SECRET_KEY"]
missing = [name for name in required_vars if not os.getenv(name)]
if missing:
raise ValueError(
"Missing required PDCL auth env vars: "
+ ", ".join(missing)
+ ". Please export them in shell or set them in .env before running."
)
def _normalize_metadata_value(value: Any) -> str:
text = str(value or "").strip()
return text if text and text.lower() != "none" else ""
def _normalize_frame_name_token(value: Any) -> str:
text = str(value or "").strip()
if not text or text.lower() == "none":
return ""
return text.replace("/", "_").replace("\\", "_").replace(" ", "")
def _format_ms_to_date_name(timestamp_ms: Any) -> str:
if not isinstance(timestamp_ms, (int, float)) or timestamp_ms <= 0:
return ""
dt = datetime.fromtimestamp(timestamp_ms / 1000, tz=timezone.utc).astimezone(BEIJING_TZ)
return dt.strftime("%Y%m%d%H%M%S")
def _build_clip_task(clip_uuid: str) -> Optional[ClipTask]:
_ensure_pdcl_auth_defaults()
from pdcl_dss import Clip
date_name = DEFAULT_DATE_NAME
vehicle_name = DEFAULT_VEHICLE_NAME
try:
clip = Clip(clip_uuid)
clip_meta = dict(clip.meta)
files, _ = clip.list_files()
if not files:
print(f"Skip clip_id={clip_uuid}: no files found")
return None
clip_path = clip.get_cache_path(files[0])
except Exception as exc:
print(f"Skip clip_id={clip_uuid}: failed to load clip metadata. {type(exc).__name__}: {exc}")
return None
clip_date_name = _format_ms_to_date_name(clip_meta.get("start_time_ms"))
if clip_date_name:
date_name = clip_date_name
try:
group_id = clip.get_group_id()
if group_id:
group_meta = dict(clip.get_group().meta)
for key in ("plate_number", "vehicle_name", "car_name", "plateNumber", "vehicleName", "carName"):
value = _normalize_metadata_value(group_meta.get(key))
if value:
vehicle_name = value
break
project_name = _normalize_metadata_value(group_meta.get("project_name"))
if vehicle_name == DEFAULT_VEHICLE_NAME and project_name:
vehicle_name = project_name
group_date_name = _format_ms_to_date_name(group_meta.get("collection_time"))
if group_date_name:
date_name = group_date_name
except Exception as exc:
print(
f"[warn] clip_id={clip_uuid}: failed to load group metadata, "
f"fallback vehicle={vehicle_name} date={date_name}. {type(exc).__name__}: {exc}"
)
return ClipTask(
clip_uuid=clip_uuid,
date_name=date_name,
vehicle_name=vehicle_name,
clip_path=clip_path,
)
def parse_clip_list(clip_list_file: str) -> list[ClipTask]:
tasks: list[ClipTask] = []
with open(clip_list_file, "r", encoding="utf-8") as file:
for line in file:
line = line.strip()
if not line or line.startswith("#"):
continue
clip_uuid = line.split()[0]
task = _build_clip_task(clip_uuid)
if task is not None:
tasks.append(task)
return tasks
def build_clip_tasks_from_clip_ids(clip_ids: list[str]) -> list[ClipTask]:
tasks: list[ClipTask] = []
seen: set[str] = set()
for clip_id in clip_ids:
clip_uuid = str(clip_id).strip()
if not clip_uuid or clip_uuid in seen:
continue
seen.add(clip_uuid)
task = _build_clip_task(clip_uuid)
if task is not None:
tasks.append(task)
return tasks
def build_clip_task_from_mcap_file(
mcap_file: str | Path,
*,
clip_id: str = "",
date_name: str = "",
vehicle_name: str = "",
) -> ClipTask:
mcap_path = Path(mcap_file).expanduser().resolve()
if not mcap_path.is_file():
raise FileNotFoundError(f"MCAP file not found: {mcap_path}")
stem = mcap_path.stem
resolved_clip_id = _normalize_metadata_value(clip_id) or stem
resolved_date_name = _normalize_metadata_value(date_name)
if not resolved_date_name and len(stem) == 14 and stem.isdigit():
resolved_date_name = stem
resolved_vehicle_name = _normalize_metadata_value(vehicle_name) or LOCAL_MCAP_VEHICLE_NAME
return ClipTask(
clip_uuid=resolved_clip_id,
date_name=resolved_date_name or DEFAULT_DATE_NAME,
vehicle_name=resolved_vehicle_name,
clip_path=str(mcap_path),
)
def build_case_dir_name(prefix: str, clip_task: ClipTask) -> str:
safe_clip_uuid = clip_task.clip_uuid.replace("/", "_")
return f"{prefix}_{clip_task.vehicle_name}_{clip_task.date_name}_{safe_clip_uuid}"
def _plane_to_ndarray(plane: Any) -> np.ndarray:
stride = plane.line_size
height = plane.height
width = plane.width
array = np.frombuffer(plane, dtype=np.uint8)
if stride == width:
return array.reshape(height, width)
return array.reshape(height, stride)[:, :width]
def _yuvj420p_to_nv12(
y_plane: np.ndarray,
u_plane: np.ndarray,
v_plane: np.ndarray,
) -> tuple[np.ndarray, np.ndarray]:
uv_height = u_plane.shape[0]
uv_width = u_plane.shape[1]
uv_plane = np.zeros((uv_height, uv_width * 2), dtype=np.uint8)
uv_plane[:, 0::2] = u_plane
uv_plane[:, 1::2] = v_plane
return y_plane.copy(), uv_plane
def _h265_payload_to_bgr(payload: bytes) -> np.ndarray:
try:
import av
except ImportError as exc:
raise ImportError("PyAV is required for mcap decoding. Please install: pip install av") from exc
container = av.open(io.BytesIO(payload))
for frame in container.decode(video=0):
y_plane = _plane_to_ndarray(frame.planes[0])
u_plane = _plane_to_ndarray(frame.planes[1])
v_plane = _plane_to_ndarray(frame.planes[2])
y_nv12, uv_nv12 = _yuvj420p_to_nv12(y_plane, u_plane, v_plane)
yuv_image = np.concatenate((y_nv12, uv_nv12), axis=0)
return cv2.cvtColor(yuv_image, cv2.COLOR_YUV2BGR_NV12)
raise ValueError("decode failed: no video frame in payload")
def _resolve_calib_file_for_clip(clip_path: str) -> Optional[Path]:
base = Path(clip_path).resolve()
search_dir = base.parent
candidates: list[Path] = []
for _ in range(4):
candidates.extend(
[
search_dir / "calibs" / "camera4.json",
search_dir / "test_data" / "calibs" / "camera4.json",
search_dir / "L2_calib" / "camera4.json",
search_dir / "calib" / "L2_calib" / "camera4.json",
]
)
parent = search_dir.parent
if parent == search_dir:
break
search_dir = parent
for calib_path in candidates:
if calib_path.exists():
return calib_path
return None
def _candidate_camera_topics(camera_topic: str) -> list[str]:
topic = str(camera_topic or "").strip()
if not topic:
return []
topics = [topic]
if "." in topic:
topics.append(topic.rsplit(".", 1)[-1])
else:
topics.append(f"sigmastar.1.{topic}")
return list(dict.fromkeys(topics))
def _resolve_reader_camera_topics(reader: Any, camera_topic: str) -> list[str]:
candidates = _candidate_camera_topics(camera_topic)
topic_map = getattr(reader, "_topic_map", None)
if isinstance(topic_map, dict):
for candidate in candidates:
if candidate in topic_map:
return [candidate]
for key, values in topic_map.items():
if any(candidate in values for candidate in candidates):
return [key]
return candidates
def _decode_mcap_to_images(
clip_uuid: str,
clip_path: str,
output_dir: Path,
camera_topic: str,
max_frames: int,
) -> tuple[int, Optional[dict[str, Any]], list[dict[str, Any]]]:
from pdcl_pyclip.decoder_struct import StructDecoder
from pdcl_pyclip.msg_camera import VideoMessage
from pdcl_pyclip.reader import ClipReader
output_dir.mkdir(parents=True, exist_ok=True)
reader = ClipReader(clip_path)
struct_decoder = StructDecoder()
saved = 0
camera4_json = None
decode_errors: list[dict[str, Any]] = []
camera_topics = _resolve_reader_camera_topics(reader, camera_topic)
for schema, channel, msg in reader.iter_messages(topics=camera_topics):
if schema.encoding != "struct":
continue
data = struct_decoder.decode(schema, channel, msg)
if not isinstance(data, VideoMessage):
continue
try:
image = _h265_payload_to_bgr(data.payload)
except Exception as exc:
frame_id = getattr(data, "frame_id", None)
error_record = {
"frame_id": None if frame_id is None else str(frame_id),
"error_type": type(exc).__name__,
"error": str(exc),
}
decode_errors.append(error_record)
print(
f"[warn] clip_id={clip_uuid} frame_id={error_record['frame_id']} "
f"-> skip bad frame: {error_record['error_type']}: {error_record['error']}"
)
continue
frame_id_token = _normalize_frame_name_token(getattr(data, "frame_id", None))
timestamp_token = _normalize_frame_name_token(getattr(msg, "log_time", None))
if frame_id_token and timestamp_token:
frame_name = f"{clip_uuid}_{frame_id_token}_{timestamp_token}.png"
elif frame_id_token:
frame_name = f"{clip_uuid}_{frame_id_token}.png"
elif timestamp_token:
frame_name = f"{clip_uuid}_{saved:06d}_{timestamp_token}.png"
else:
frame_name = f"{clip_uuid}_{saved:06d}.png"
if not cv2.imwrite(str(output_dir / frame_name), image):
raise IOError(f"Failed to write image: {output_dir / frame_name}")
saved += 1
if max_frames > 0 and saved >= max_frames:
break
for attachment in reader.iter_attachments():
if attachment.name in CALIB_ATTACHMENT_NAMES:
camera4_json = json.loads(attachment.data.decode("utf-8"))
break
return saved, camera4_json, decode_errors
def _save_decode_errors(output_dir: Path, clip_uuid: str, decode_errors: list[dict[str, Any]]) -> Optional[Path]:
if not decode_errors:
return None
output_dir.mkdir(parents=True, exist_ok=True)
decode_errors_path = output_dir / "decode_errors.json"
payload = {
"clip_uuid": clip_uuid,
"skipped_bad_frame_count": len(decode_errors),
"errors": decode_errors,
}
with open(decode_errors_path, "w", encoding="utf-8") as file:
json.dump(payload, file, indent=2, ensure_ascii=False)
return decode_errors_path
def _save_calib_for_clip(
clip_path: str,
calib_output_path: Path,
calib_file_override: str,
embedded_calib: Optional[dict[str, Any]],
) -> str:
calib_output_path.parent.mkdir(parents=True, exist_ok=True)
if calib_file_override:
calib_src = Path(calib_file_override).resolve()
if not calib_src.exists():
raise FileNotFoundError(f"Calibration file {calib_src} does not exist.")
calib_output_path.write_bytes(calib_src.read_bytes())
return f"override:{calib_src}"
calib_src = _resolve_calib_file_for_clip(clip_path)
if calib_src is not None:
calib_output_path.write_bytes(calib_src.read_bytes())
return f"file:{calib_src}"
if embedded_calib is None:
raise FileNotFoundError(
"Calibration file camera4.json not found near clip and not found in mcap attachments. "
"Please provide --calib-file explicitly."
)
with open(calib_output_path, "w", encoding="utf-8") as file:
json.dump(embedded_calib, file, indent=2, ensure_ascii=False)
return "mcap_attachment"
def _build_calib_summary(calib_payload: Optional[dict[str, Any]]) -> dict[str, Any]:
if not isinstance(calib_payload, dict):
return {
"format": "unknown",
"has_distortion": False,
}
intrinsics = calib_payload
extrinsics = None
calib_format = "flat_camera4"
if "intrinsics" in calib_payload:
intrinsics = calib_payload.get("intrinsics", {}).get("camera4.json", {}) or {}
extrinsics = calib_payload.get("extrinsics", {}).get("camera4.json", {}) or {}
calib_format = "combined_calibration"
distort_coeffs = intrinsics.get("distort_coeffs", calib_payload.get("distort_coeffs", [])) or []
return {
"format": calib_format,
"focal_u": intrinsics.get("focal_u"),
"focal_v": intrinsics.get("focal_v"),
"cu": intrinsics.get("cu"),
"cv": intrinsics.get("cv"),
"pitch": calib_payload.get("pitch", extrinsics.get("rpy", [None, None, None])[1] if extrinsics else None),
"yaw": calib_payload.get("yaw", extrinsics.get("rpy", [None, None, None])[2] if extrinsics else None),
"image_width": calib_payload.get("image_width", calib_payload.get("img_width")),
"image_height": calib_payload.get("image_height", calib_payload.get("img_height")),
"distort_coeff_count": len(distort_coeffs),
"has_distortion": bool(distort_coeffs),
}
def _save_export_manifest(
output_dir: Path,
clip_task: ClipTask,
frame_count: int,
calib_source: str,
camera_topic: str,
calib_summary: Optional[dict[str, Any]] = None,
skipped_bad_frame_count: int = 0,
decode_errors_path: Optional[str] = None,
) -> None:
manifest = {
"clip_uuid": clip_task.clip_uuid,
"date_name": clip_task.date_name,
"vehicle_name": clip_task.vehicle_name,
"clip_path": clip_task.clip_path,
"camera_topic": camera_topic,
"frame_count": frame_count,
"skipped_bad_frame_count": skipped_bad_frame_count,
"calib_source": calib_source,
"calib_summary": calib_summary,
}
if decode_errors_path:
manifest["decode_errors_path"] = decode_errors_path
with open(output_dir / "manifest.json", "w", encoding="utf-8") as file:
json.dump(manifest, file, indent=2, ensure_ascii=False)
def _input_source_label(args: argparse.Namespace) -> str:
return str(getattr(args, "mcap_file", "") or getattr(args, "clip_list_file", ""))
def _input_source_mode(args: argparse.Namespace) -> str:
if getattr(args, "mcap_file", ""):
return "mcap_file"
return "clip_list"
def _save_calib_summary(output_dir: Path, calib_source: str, calib_summary: dict[str, Any]) -> None:
payload = {
"calib_source": calib_source,
"calib_summary": calib_summary,
}
with open(output_dir / "calib_summary.json", "w", encoding="utf-8") as file:
json.dump(payload, file, indent=2, ensure_ascii=False)
def export_one_clip(
clip_task: ClipTask,
args: argparse.Namespace,
status: StatusStore,
) -> TaskResult:
task_id = clip_task.task_id
if args.skip_done and status.is_done(task_id):
info = status.get(task_id) or {}
return TaskResult(task_id=task_id, success=True, message=f"skip done: {info.get('detail', '')}")
case_dir = Path(args.output_root) / build_case_dir_name(args.output_prefix, clip_task)
images_dir = case_dir / "images"
calib_path = case_dir / "calib" / "L2_calib" / "camera4.json"
status.mark_running(task_id, f"running source={clip_task.clip_path}")
try:
frame_count, embedded_calib, decode_errors = _decode_mcap_to_images(
clip_uuid=clip_task.clip_uuid,
clip_path=clip_task.clip_path,
output_dir=images_dir,
camera_topic=args.camera_topic,
max_frames=args.max_frames_per_clip,
)
decode_errors_path = _save_decode_errors(case_dir, clip_task.clip_uuid, decode_errors)
if frame_count <= 0:
if decode_errors:
first_error = decode_errors[0]
raise RuntimeError(
f"Failed to decode any frames; skipped {len(decode_errors)} bad frames. "
f"First error: {first_error['error_type']}: {first_error['error']}. "
f"See {decode_errors_path}"
)
raise RuntimeError(
f"No frames were decoded from clip {clip_task.clip_uuid} on topic "
f"{args.camera_topic!r} (tried: {', '.join(_candidate_camera_topics(args.camera_topic))})."
)
calib_source = _save_calib_for_clip(
clip_path=clip_task.clip_path,
calib_output_path=calib_path,
calib_file_override=args.calib_file,
embedded_calib=embedded_calib,
)
with open(calib_path, "r", encoding="utf-8") as file:
calib_payload = json.load(file)
calib_summary = _build_calib_summary(calib_payload)
_save_export_manifest(
output_dir=case_dir,
clip_task=clip_task,
frame_count=frame_count,
calib_source=calib_source,
camera_topic=args.camera_topic,
calib_summary=calib_summary,
skipped_bad_frame_count=len(decode_errors),
decode_errors_path=str(decode_errors_path) if decode_errors_path else None,
)
_save_calib_summary(case_dir, calib_source=calib_source, calib_summary=calib_summary)
status.mark_done(task_id, str(case_dir))
return TaskResult(
task_id=task_id,
success=True,
message=f"ok frames={frame_count} skipped_bad_frames={len(decode_errors)}",
output_dir=str(case_dir),
)
except Exception as exc:
message = f"{type(exc).__name__}: {exc}"
status.mark_failed(task_id, message)
err_log = Path(args.output_root) / "_status" / "errors.log"
err_log.parent.mkdir(parents=True, exist_ok=True)
with open(err_log, "a", encoding="utf-8") as file:
file.write(f"[{task_id}] {message}\n{traceback.format_exc()}\n")
return TaskResult(task_id=task_id, success=False, message=message)
def save_run_manifest(args: argparse.Namespace, clip_tasks: list[ClipTask]) -> None:
manifest_path = Path(args.output_root) / "_status" / "run_manifest.json"
manifest_path.parent.mkdir(parents=True, exist_ok=True)
payload = {
"source_mode": _input_source_mode(args),
"source": _input_source_label(args),
"clip_list_file": getattr(args, "clip_list_file", ""),
"mcap_file": getattr(args, "mcap_file", ""),
"output_root": args.output_root,
"camera_topic": args.camera_topic,
"max_frames_per_clip": args.max_frames_per_clip,
"num_tasks": len(clip_tasks),
"tasks": [
{
"task_id": task.task_id,
"clip_uuid": task.clip_uuid,
"date_name": task.date_name,
"vehicle_name": task.vehicle_name,
"clip_path": task.clip_path,
}
for task in clip_tasks
],
}
with open(manifest_path, "w", encoding="utf-8") as file:
json.dump(payload, file, indent=2, ensure_ascii=False)
def has_reusable_exported_case(case_dir: Path) -> bool:
images_dir = case_dir / "images"
calib_path = case_dir / "calib" / "L2_calib" / "camera4.json"
if not images_dir.is_dir() or not calib_path.is_file():
return False
return any(path.is_file() for path in images_dir.iterdir())
def save_exported_batch_manifest(args: argparse.Namespace, clip_tasks: list[ClipTask], output_root: Path) -> None:
manifest_path = output_root / "_status" / "run_manifest.json"
manifest_path.parent.mkdir(parents=True, exist_ok=True)
payload = {
"source_mode": _input_source_mode(args),
"source": _input_source_label(args),
"clip_list_file": getattr(args, "clip_list_file", ""),
"mcap_file": getattr(args, "mcap_file", ""),
"export_root": args.export_root,
"output_dir": str(output_root),
"camera_topic": args.camera_topic,
"max_frames_per_clip": args.max_frames_per_clip,
"exported_model": args.exported_model,
"edge_yaw_max_lateral_dist_m": args.edge_yaw_max_lateral_dist,
"roi_models": {
"roi0": args.roi0_model,
"roi1": args.roi1_model,
},
"num_tasks": len(clip_tasks),
"tasks": [
{
"task_id": task.task_id,
"clip_uuid": task.clip_uuid,
"date_name": task.date_name,
"vehicle_name": task.vehicle_name,
"clip_path": task.clip_path,
}
for task in clip_tasks
],
}
with manifest_path.open("w", encoding="utf-8") as file:
json.dump(payload, file, indent=2, ensure_ascii=False)
def run_clip_tasks_inference_exported(
*,
context: Any,
args: argparse.Namespace,
clip_tasks: list[ClipTask],
load_env: Callable[[], Any],
run_case_inference: Callable[..., dict[str, Any]],
export_root: str | Path | None = None,
output_root: str | Path | None = None,
on_before_run: Optional[Callable[[Path, Path, list[ClipTask]], None]] = None,
require_pdcl_auth: bool = True,
) -> dict[str, Any]:
load_env()
if require_pdcl_auth:
validate_pdcl_auth_env()
export_root = Path(args.export_root if export_root is None else export_root).resolve()
output_root = Path(args.output_dir if output_root is None else output_root).resolve()
export_root.mkdir(parents=True, exist_ok=True)
output_root.mkdir(parents=True, exist_ok=True)
args.output_root = str(export_root)
if not clip_tasks:
print("No clip tasks discovered. Exit.")
return {
"source_mode": _input_source_mode(args),
"source": _input_source_label(args),
"clip_list_file": getattr(args, "clip_list_file", ""),
"mcap_file": getattr(args, "mcap_file", ""),
"output_dir": str(output_root),
"total": 0,
"success": 0,
"failed": 0,
}
export_status = StatusStore(export_root / "_status" / "task_status.json")
infer_status = StatusStore(output_root / "_status" / "task_status.json")
if on_before_run is not None:
on_before_run(export_root, output_root, clip_tasks)
save_run_manifest(args, clip_tasks)
save_exported_batch_manifest(args, clip_tasks, output_root)
print(f"Discovered {len(clip_tasks)} clips.")
print(f"Export root: {export_root}")
print(f"Visualization root: {output_root}")
success = 0
fail = 0
for index, clip_task in enumerate(clip_tasks, start=1):
task_id = clip_task.task_id
if args.skip_done and infer_status.is_done(task_id):
info = infer_status.get(task_id) or {}
print(f"[{index}/{len(clip_tasks)}] clip_id={task_id} -> skip done: {info.get('detail', '')}")
continue
print(f"[{index}/{len(clip_tasks)}] clip_id={task_id} source={clip_task.clip_path}")
infer_status.mark_running(task_id, "exporting clip and running exported two-roi inference")
try:
case_dir = export_root / build_case_dir_name(args.output_prefix, clip_task)
if has_reusable_exported_case(case_dir):
export_status.mark_done(task_id, f"reuse existing export: {case_dir}")
print(f" -> reuse exported clip data from {case_dir}")
else:
export_result = export_one_clip(clip_task, args, export_status)
if not export_result.success or not export_result.output_dir:
raise RuntimeError(export_result.message)
case_dir = Path(export_result.output_dir)
infer_result = run_case_inference(
context=context,
case_dir=str(case_dir),
output_dir=output_root / case_dir.name,
glob_pattern=args.glob,
max_images=args.max_images,
save_aggregate_predictions=bool(getattr(args, "save_aggregate_predictions", False)),
save_visualizations=not bool(getattr(args, "skip_visualizations", False)),
)
infer_status.mark_done(task_id, infer_result["output_dir"])
success += 1
print(f" -> saved {infer_result['num_frames']} frames to {infer_result['output_dir']}")
except Exception as exc:
fail += 1
message = f"{type(exc).__name__}: {exc}"
infer_status.mark_failed(task_id, message)
err_log = output_root / "_status" / "errors.log"
err_log.parent.mkdir(parents=True, exist_ok=True)
with err_log.open("a", encoding="utf-8") as file:
file.write(f"[{task_id}] {message}\n{traceback.format_exc()}\n")
print(f" -> failed: {message}")
print("\nBatch inference done.")
print(f"success={success}, fail={fail}")
print(f"infer_status_summary={infer_status.summary()}")
return {
"source_mode": _input_source_mode(args),
"source": _input_source_label(args),
"clip_list_file": getattr(args, "clip_list_file", ""),
"mcap_file": getattr(args, "mcap_file", ""),
"export_root": str(export_root),
"output_dir": str(output_root),
"total": len(clip_tasks),
"success": success,
"failed": fail,
}
def run_clip_list_inference_exported(
*,
context: Any,
args: argparse.Namespace,
load_env: Callable[[], Any],
run_case_inference: Callable[..., dict[str, Any]],
) -> dict[str, Any]:
clip_tasks = parse_clip_list(args.clip_list_file)
if args.limit_clips > 0:
clip_tasks = clip_tasks[: args.limit_clips]
return run_clip_tasks_inference_exported(
context=context,
args=args,
clip_tasks=clip_tasks,
load_env=load_env,
run_case_inference=run_case_inference,
)
def run_mcap_file_inference_exported(
*,
context: Any,
args: argparse.Namespace,
load_env: Callable[[], Any],
run_case_inference: Callable[..., dict[str, Any]],
) -> dict[str, Any]:
clip_task = build_clip_task_from_mcap_file(
args.mcap_file,
clip_id=args.mcap_clip_id,
date_name=args.mcap_date_name,
vehicle_name=args.mcap_vehicle_name,
)
result = run_clip_tasks_inference_exported(
context=context,
args=args,
clip_tasks=[clip_task],
load_env=load_env,
run_case_inference=run_case_inference,
require_pdcl_auth=False,
)
if result.get("failed", 0) > 0 or result.get("success", 0) < result.get("total", 0):
raise RuntimeError(f"MCAP inference failed for {args.mcap_file}")
return result

View File

@@ -0,0 +1,247 @@
from __future__ import annotations
import json
from pathlib import Path
from typing import Any, Iterable, Optional
import cv2
import numpy as np
DEFAULT_CNCAP_PATH_PREFIX_SRC = "/mnt/hfs/project-G1M3"
DEFAULT_CNCAP_PATH_PREFIX_DST = "/mnt/G1M3"
DEFAULT_OUTPUT_RELATIVE_ANCHORS = ("CNCAP2024数采", "gt_org_data")
def rewrite_path_prefix(path_str: str, prefix_src: str, prefix_dst: str) -> str:
normalized_path = str(path_str).strip()
normalized_src = str(prefix_src).rstrip("/")
normalized_dst = str(prefix_dst).rstrip("/")
if normalized_src and normalized_path.startswith(normalized_src):
return f"{normalized_dst}{normalized_path[len(normalized_src):]}"
return normalized_path
def load_path_list_from_json(json_file: str | Path, values_key: str = "values") -> list[str]:
json_path = Path(json_file).resolve()
with json_path.open("r", encoding="utf-8") as file:
payload = json.load(file)
if not isinstance(payload, dict):
raise ValueError(f"Expected top-level dict in {json_path}, got {type(payload).__name__}")
values = payload.get(values_key)
if not isinstance(values, list):
raise ValueError(f"Expected {values_key!r} list in {json_path}, got {type(values).__name__}")
return [str(item).strip() for item in values if str(item).strip()]
def _normalize_video_case_input(video_case_dir: str | Path) -> Path:
input_path = Path(video_case_dir).resolve()
if input_path.is_file() and input_path.name == "camera4.bin":
return input_path.parent.parent
if input_path.is_dir() and input_path.name == "sigmastar.1":
return input_path.parent
return input_path
def build_case_output_rel_dir(
case_dir: str | Path,
preferred_anchor_names: Iterable[str] = DEFAULT_OUTPUT_RELATIVE_ANCHORS,
) -> Path:
resolved_case_dir = Path(case_dir).resolve()
parts = resolved_case_dir.parts
for anchor_name in preferred_anchor_names:
if anchor_name in parts:
anchor_index = parts.index(anchor_name)
suffix_parts = parts[anchor_index + 1 :]
if suffix_parts:
return Path(*suffix_parts)
if len(parts) >= 3:
return Path(*parts[-3:])
if len(parts) >= 2:
return Path(*parts[-2:])
if parts:
return Path(parts[-1])
return Path("case")
def resolve_video_case_paths(video_case_dir: str | Path) -> tuple[Path, Path, Path]:
case_dir = _normalize_video_case_input(video_case_dir)
if not case_dir.is_dir():
raise FileNotFoundError(f"Video case directory not found: {case_dir}")
video_path = case_dir / "sigmastar.1" / "camera4.bin"
if not video_path.is_file():
raise FileNotFoundError(f"camera4.bin not found under {case_dir}")
calib_candidates = [
case_dir / "test_data" / "calibs" / "camera4.json",
case_dir.parent / "test_data" / "calibs" / "camera4.json",
case_dir / "sigmastar.1" / "calibs" / "camera4.json",
case_dir / "calibs" / "camera4.json",
]
calib_path = next((path for path in calib_candidates if path.is_file()), None)
if calib_path is None:
checked = ", ".join(str(path) for path in calib_candidates)
raise FileNotFoundError(f"camera4.json not found for {case_dir}. Checked: {checked}")
return case_dir, video_path, calib_path
def collect_video_case_dirs(video_root_dir: str | Path) -> list[Path]:
root_dir = Path(video_root_dir).resolve()
if not root_dir.is_dir():
raise FileNotFoundError(f"Video root directory not found: {root_dir}")
case_dirs: list[Path] = []
for path in sorted(root_dir.iterdir()):
if not path.is_dir():
continue
try:
resolve_video_case_paths(path)
except FileNotFoundError:
continue
case_dirs.append(path)
if not case_dirs:
raise FileNotFoundError(f"No valid video case directories found under {root_dir}")
return case_dirs
def read_video_frame_index(video_path: str | Path) -> Optional[dict[str, Any]]:
try:
video_path_obj = Path(video_path).resolve()
case_dir = video_path_obj.parent.parent
video_folder = video_path_obj.parent.name
video_file = video_path_obj.stem
index_path = case_dir / "L2" / f"{video_folder}.{video_file}.index.json"
if not index_path.is_file():
return None
with index_path.open("r", encoding="utf-8") as file:
payload = json.load(file)
return payload if isinstance(payload, dict) else None
except Exception:
return None
def get_video_frame_info(frame_index_payload: Optional[dict[str, Any]], frame_idx: int) -> Optional[dict[str, Any]]:
if not frame_index_payload:
return None
try:
fields = frame_index_payload.get("fields", {})
index_list = frame_index_payload.get("index", [])
if not isinstance(fields, dict) or not isinstance(index_list, list) or frame_idx >= len(index_list):
return None
frame_data = index_list[frame_idx]
if not isinstance(frame_data, (list, tuple)):
return None
frame_info = {}
for field_name, field_idx in fields.items():
if isinstance(field_idx, int) and 0 <= field_idx < len(frame_data):
frame_info[str(field_name)] = frame_data[field_idx]
return frame_info
except Exception:
return None
def _normalize_frame_info_token(value: Any) -> str:
token = str(value or "").strip()
if not token or token.lower() == "none":
return ""
return token.replace("/", "_").replace("\\", "_").replace(" ", "")
def _safe_int(value: Any) -> Optional[int]:
try:
if value is None:
return None
return int(str(value).strip())
except (TypeError, ValueError):
return None
def get_video_frame_id(frame_info: Optional[dict[str, Any]]) -> Optional[int]:
if not frame_info:
return None
for key in ("frame_id", "cve_frame_id", "frameId"):
frame_id = _safe_int(frame_info.get(key))
if frame_id is not None:
return frame_id
return None
def iter_video_case_frames(
video_path: str | Path,
*,
frame_index_payload: Optional[dict[str, Any]] = None,
frame_stride: int = 1,
max_frames: int = 0,
frame_index_start: Optional[int] = None,
frame_index_end: Optional[int] = None,
frame_id_start: Optional[int] = None,
frame_id_end: Optional[int] = None,
) -> Iterable[tuple[int, np.ndarray, str, Optional[dict[str, Any]]]]:
resolved_video_path = Path(video_path).resolve()
cap = cv2.VideoCapture(str(resolved_video_path))
if not cap.isOpened():
raise RuntimeError(f"Failed to open video file: {resolved_video_path}")
stride = max(1, int(frame_stride))
resolved_frame_index_start = None if frame_index_start is None else max(0, int(frame_index_start))
resolved_frame_index_end = None if frame_index_end is None else int(frame_index_end)
resolved_frame_id_start = None if frame_id_start is None else int(frame_id_start)
resolved_frame_id_end = None if frame_id_end is None else int(frame_id_end)
read_frame_index = 0
emitted_count = 0
try:
while True:
ret, frame = cap.read()
if not ret:
break
if resolved_frame_index_end is not None and read_frame_index > resolved_frame_index_end:
break
frame_info = get_video_frame_info(frame_index_payload, read_frame_index)
frame_id_value = get_video_frame_id(frame_info)
if resolved_frame_index_start is not None and read_frame_index < resolved_frame_index_start:
read_frame_index += 1
continue
if resolved_frame_id_start is not None:
if frame_id_value is None or frame_id_value < resolved_frame_id_start:
read_frame_index += 1
continue
if resolved_frame_id_end is not None and frame_id_value is not None and frame_id_value > resolved_frame_id_end:
break
if read_frame_index % stride != 0:
read_frame_index += 1
continue
frame_id_token = _normalize_frame_info_token(frame_id_value)
timestamp_token = _normalize_frame_info_token(None if frame_info is None else frame_info.get("timestamp"))
if frame_id_token and timestamp_token:
frame_name = f"{resolved_video_path.stem}_{frame_id_token}_{timestamp_token}.png"
elif frame_id_token:
frame_name = f"{resolved_video_path.stem}_{frame_id_token}.png"
elif timestamp_token:
frame_name = f"{resolved_video_path.stem}_{read_frame_index:06d}_{timestamp_token}.png"
else:
frame_name = f"{resolved_video_path.stem}_{read_frame_index:06d}.png"
yield read_frame_index, frame, frame_name, frame_info
emitted_count += 1
read_frame_index += 1
if max_frames > 0 and emitted_count >= max_frames:
break
finally:
cap.release()