单目3D初始代码
This commit is contained in:
1
tools/model_inference/adapters/__init__.py
Executable file
1
tools/model_inference/adapters/__init__.py
Executable file
@@ -0,0 +1 @@
|
||||
"""Input adapters for model_inference batch sources."""
|
||||
822
tools/model_inference/adapters/eventid_clip_resolver.py
Executable file
822
tools/model_inference/adapters/eventid_clip_resolver.py
Executable 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
|
||||
141
tools/model_inference/adapters/get_clip_by_eventid.py
Executable file
141
tools/model_inference/adapters/get_clip_by_eventid.py
Executable 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()
|
||||
826
tools/model_inference/adapters/pdcl_clip_export_utils.py
Executable file
826
tools/model_inference/adapters/pdcl_clip_export_utils.py
Executable 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
|
||||
247
tools/model_inference/adapters/video_dir_inference_utils.py
Executable file
247
tools/model_inference/adapters/video_dir_inference_utils.py
Executable 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()
|
||||
Reference in New Issue
Block a user