Files
yolov26_3d/tools/feishu_project/decode_issue_frame_window.py
2026-06-24 09:35:46 +08:00

839 lines
33 KiB
Python
Executable File

#!/usr/bin/env python3
"""Decode 61-frame windows around issue frame ids for D4Q2 network-share cases."""
from __future__ import annotations
import argparse
import io
import json
import re
import sys
from collections import Counter, deque
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Any, Iterable, Optional
import cv2
import numpy as np
FILE = Path(__file__).resolve()
ROOT = FILE.parents[2]
if str(ROOT) not in sys.path:
sys.path.append(str(ROOT))
from tools.model_inference.adapters.video_dir_inference_utils import (
get_video_frame_info,
read_video_frame_index,
)
try:
from pdcl_pyclip.decoder_struct import StructDecoder
from pdcl_pyclip.msg_camera import VideoMessage
from pdcl_pyclip.reader import ClipReader
except ImportError:
ClipReader = None
StructDecoder = None
VideoMessage = None
NETWORK_SHARE_MARKERS = ("hfs.minieye.tech", "192.168.2.122")
WINDOW_RADIUS = 30
WINDOW_SIZE = WINDOW_RADIUS * 2 + 1
DEFAULT_INPUT_JSON = ROOT / "tools" / "feishu_project" / "dongying_d4q2_zhibao_issue_list.json"
DEFAULT_DOWNLOAD_ROOT = Path("/data1/dongying/Mono3d/D4Q2/feishu_project/downloaded_issue_data")
DEFAULT_OUTPUT_ROOT = Path("/data1/dongying/Mono3d/D4Q2/feishu_project/decoded_issue_frame_windows")
FRAME_ID_CAMERA4_RE = re.compile(r"camera4\s*:\s*(\d+)", re.IGNORECASE)
FRAME_ID_ANY_CAMERA_RE = re.compile(r"(camera\d+)\s*:\s*(\d+)", re.IGNORECASE)
PURE_DIGIT_RE = re.compile(r"^\d+$")
@dataclass(frozen=True)
class TargetFrame:
camera: str
frame_id: int
raw_text: str
@dataclass(frozen=True)
class CaseSource:
issue_id: int
issue_name: str
issue_dir: Path
path_dir: Path
case_dir: Path
relative_case_dir: Path
target_frame: TargetFrame
decode_all: bool
source_mode: str
source_paths: tuple[Path, ...]
@dataclass
class CaseResult:
issue_id: int
issue_name: str
case_dir: str
relative_case_dir: str
target_camera: str
target_frame_id: int | None
decode_mode: str
source_mode: str | None
source_paths: list[str]
output_dir: str
status: str
detail: str
matched_field: str | None = None
matched_frame_idx: int | None = None
matched_topic: str | None = None
extracted_count: int = 0
def to_dict(self) -> dict[str, Any]:
return {
"issue_id": self.issue_id,
"issue_name": self.issue_name,
"case_dir": self.case_dir,
"relative_case_dir": self.relative_case_dir,
"target_camera": self.target_camera,
"target_frame_id": self.target_frame_id,
"decode_mode": self.decode_mode,
"source_mode": self.source_mode,
"source_paths": self.source_paths,
"output_dir": self.output_dir,
"status": self.status,
"detail": self.detail,
"matched_field": self.matched_field,
"matched_frame_idx": self.matched_frame_idx,
"matched_topic": self.matched_topic,
"extracted_count": self.extracted_count,
}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Decode 61-frame windows around issue frame ids for downloaded D4Q2 network-share cases."
)
parser.add_argument("--input-json", default=str(DEFAULT_INPUT_JSON))
parser.add_argument("--download-root", default=str(DEFAULT_DOWNLOAD_ROOT))
parser.add_argument("--output-root", default=str(DEFAULT_OUTPUT_ROOT))
parser.add_argument("--manifest-path", default="")
parser.add_argument("--issue-id", action="append", dest="issue_ids", type=int)
parser.add_argument("--decode-all-issue-id", action="append", dest="decode_all_issue_ids", type=int)
parser.add_argument("--window-radius", type=int, default=WINDOW_RADIUS)
parser.add_argument("--jpg-quality", type=int, default=95)
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--skip-existing", action="store_true")
return parser.parse_args()
def ensure_dir(path: Path, dry_run: bool) -> None:
if dry_run:
return
path.mkdir(parents=True, exist_ok=True)
def load_issue_items(path: Path) -> list[dict[str, Any]]:
payload = json.loads(path.read_text(encoding="utf-8"))
return payload["items"]
def parse_target_frame(frame_text: object) -> TargetFrame | None:
text = "" if frame_text is None else str(frame_text).strip()
if not text:
return None
camera4_match = FRAME_ID_CAMERA4_RE.search(text)
if camera4_match:
return TargetFrame(camera="camera4", frame_id=int(camera4_match.group(1)), raw_text=text)
if PURE_DIGIT_RE.fullmatch(text):
return TargetFrame(camera="any", frame_id=int(text), raw_text=text)
any_camera_match = FRAME_ID_ANY_CAMERA_RE.search(text)
if any_camera_match:
return TargetFrame(camera=any_camera_match.group(1).lower(), frame_id=int(any_camera_match.group(2)), raw_text=text)
return None
def is_network_share_issue(item: dict[str, Any]) -> bool:
address = str(item.get("问题数据地址") or "")
return any(marker in address for marker in NETWORK_SHARE_MARKERS)
def find_candidate_mcaps(case_dir: Path) -> list[Path]:
root_level = sorted(
path
for path in case_dir.iterdir()
if path.is_file() and path.suffix.lower() == ".mcap" and "_PB_" not in path.name
)
if root_level:
return root_level
recursive = sorted(
path
for path in case_dir.rglob("*.mcap")
if path.is_file() and "_PB_" not in path.name
)
return recursive
def find_camera4_bin(case_dir: Path) -> Path | None:
candidates = sorted(case_dir.rglob("camera4.bin"), key=lambda p: (len(p.relative_to(case_dir).parts), str(p)))
return candidates[0] if candidates else None
def discover_case_sources(
items: list[dict[str, Any]],
download_root: Path,
issue_filter: set[int] | None,
decode_all_issue_filter: set[int],
) -> tuple[list[CaseSource], list[CaseResult]]:
discovered: list[CaseSource] = []
skipped: list[CaseResult] = []
for item in items:
issue_id = int(item["id"])
if issue_filter and issue_id not in issue_filter:
continue
if not is_network_share_issue(item):
continue
issue_name = str(item["name"])
issue_dir = download_root / f"issue_{issue_id}"
target_frame = parse_target_frame(item.get("问题发生frameid"))
if target_frame is None:
skipped.append(
CaseResult(
issue_id=issue_id,
issue_name=issue_name,
case_dir="",
relative_case_dir="",
target_camera="",
target_frame_id=None,
decode_mode="window",
source_mode=None,
source_paths=[],
output_dir="",
status="skipped_missing_frame_id",
detail=f"unparseable frame id: {item.get('问题发生frameid')!r}",
)
)
continue
if target_frame.camera not in {"camera4", "camera1", "any"}:
skipped.append(
CaseResult(
issue_id=issue_id,
issue_name=issue_name,
case_dir="",
relative_case_dir="",
target_camera=target_frame.camera,
target_frame_id=target_frame.frame_id,
decode_mode="all" if issue_id in decode_all_issue_filter else "window",
source_mode=None,
source_paths=[],
output_dir="",
status="skipped_unsupported_camera",
detail=f"unsupported camera selector in frame id: {target_frame.raw_text}",
)
)
continue
if not issue_dir.is_dir():
skipped.append(
CaseResult(
issue_id=issue_id,
issue_name=issue_name,
case_dir=str(issue_dir),
relative_case_dir=str(issue_dir.name),
target_camera=target_frame.camera,
target_frame_id=target_frame.frame_id,
decode_mode="all" if issue_id in decode_all_issue_filter else "window",
source_mode=None,
source_paths=[],
output_dir="",
status="skipped_missing_issue_dir",
detail=f"issue download directory not found: {issue_dir}",
)
)
continue
path_dirs = sorted(path for path in issue_dir.iterdir() if path.is_dir() and path.name.startswith("path_"))
if not path_dirs:
skipped.append(
CaseResult(
issue_id=issue_id,
issue_name=issue_name,
case_dir=str(issue_dir),
relative_case_dir=str(issue_dir.name),
target_camera=target_frame.camera,
target_frame_id=target_frame.frame_id,
decode_mode="all" if issue_id in decode_all_issue_filter else "window",
source_mode=None,
source_paths=[],
output_dir="",
status="skipped_no_path_cases",
detail="no path_* directories found for network-share issue",
)
)
continue
for path_dir in path_dirs:
case_dirs = sorted(path for path in path_dir.iterdir() if path.is_dir())
if not case_dirs:
skipped.append(
CaseResult(
issue_id=issue_id,
issue_name=issue_name,
case_dir=str(path_dir),
relative_case_dir=str(path_dir.relative_to(download_root)),
target_camera=target_frame.camera,
target_frame_id=target_frame.frame_id,
decode_mode="all" if issue_id in decode_all_issue_filter else "window",
source_mode=None,
source_paths=[],
output_dir="",
status="skipped_empty_path_dir",
detail="path_* directory does not contain any case subdirectory",
)
)
continue
for case_dir in case_dirs:
relative_case_dir = case_dir.relative_to(download_root)
candidate_mcaps = find_candidate_mcaps(case_dir)
if candidate_mcaps:
discovered.append(
CaseSource(
issue_id=issue_id,
issue_name=issue_name,
issue_dir=issue_dir,
path_dir=path_dir,
case_dir=case_dir,
relative_case_dir=relative_case_dir,
target_frame=target_frame,
decode_all=issue_id in decode_all_issue_filter,
source_mode="mcap_stream",
source_paths=tuple(candidate_mcaps),
)
)
continue
camera4_bin = find_camera4_bin(case_dir)
if camera4_bin is not None:
discovered.append(
CaseSource(
issue_id=issue_id,
issue_name=issue_name,
issue_dir=issue_dir,
path_dir=path_dir,
case_dir=case_dir,
relative_case_dir=relative_case_dir,
target_frame=target_frame,
decode_all=issue_id in decode_all_issue_filter,
source_mode="camera4_bin",
source_paths=(camera4_bin,),
)
)
continue
skipped.append(
CaseResult(
issue_id=issue_id,
issue_name=issue_name,
case_dir=str(case_dir),
relative_case_dir=str(relative_case_dir),
target_camera=target_frame.camera,
target_frame_id=target_frame.frame_id,
decode_mode="all" if issue_id in decode_all_issue_filter else "window",
source_mode=None,
source_paths=[],
output_dir="",
status="skipped_no_source",
detail="no non-PB .mcap or camera4.bin found under case directory",
)
)
return discovered, skipped
def _plane_to_ndarray(plane) -> 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 _h265_payload_to_bgr(payload: bytes) -> np.ndarray:
try:
import av
except ImportError as exc:
raise ImportError("PyAV is required for MCAP frame decoding.") 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])
uv_plane = np.zeros((u_plane.shape[0], u_plane.shape[1] * 2), dtype=np.uint8)
uv_plane[:, 0::2] = u_plane
uv_plane[:, 1::2] = v_plane
yuv_image = np.concatenate((y_plane.copy(), uv_plane), axis=0)
return cv2.cvtColor(yuv_image, cv2.COLOR_YUV2BGR_NV12)
raise ValueError("decode failed: no video frame in payload")
def iter_mcap_frames(mcap_paths: Iterable[Path], topic_candidates: tuple[str, ...]) -> Iterable[dict[str, Any]]:
if ClipReader is None or StructDecoder is None or VideoMessage is None:
raise ImportError("pdcl_pyclip is required for MCAP extraction.")
decoder = StructDecoder()
for mcap_path in mcap_paths:
reader = ClipReader(str(mcap_path))
for schema, channel, msg in reader.iter_messages():
data = decoder.decode(schema, channel, msg)
if not isinstance(data, VideoMessage):
continue
frame_id = getattr(data, "frame_id", None)
if frame_id is None:
continue
yield {
"source_path": mcap_path,
"topic": getattr(channel, "topic", ""),
"frame_id": int(frame_id),
"payload": data.payload,
"timestamp": str(getattr(data, "timestamp", "")),
}
def topic_matches_camera(topic: str, target_camera: str) -> bool:
normalized_topic = str(topic or "").lower()
if target_camera == "any":
return "camera" in normalized_topic
return target_camera in normalized_topic
def collect_mcap_window(
mcap_paths: tuple[Path, ...],
target_camera: str,
target_frame_id: int,
window_radius: int,
) -> tuple[list[dict[str, Any]], str, str]:
buffer_before: deque[dict[str, Any]] = deque(maxlen=window_radius)
selected: list[dict[str, Any]] = []
trailing_needed = window_radius
found = False
matched_topic = ""
for frame in iter_mcap_frames(mcap_paths, tuple()):
if not topic_matches_camera(frame["topic"], target_camera):
continue
if not found:
if frame["frame_id"] == target_frame_id:
selected = list(buffer_before) + [frame]
found = True
matched_topic = str(frame["topic"])
else:
buffer_before.append(frame)
else:
selected.append(frame)
trailing_needed -= 1
if trailing_needed <= 0:
break
if not found:
raise FileNotFoundError(f"target frame_id {target_frame_id} not found in MCAP sources for camera selector {target_camera}")
return selected, "frame_id", matched_topic
def collect_mcap_all_frames(
mcap_paths: tuple[Path, ...],
target_camera: str,
) -> tuple[list[dict[str, Any]], str]:
selected: list[dict[str, Any]] = []
matched_topic = ""
for frame in iter_mcap_frames(mcap_paths, tuple()):
if not topic_matches_camera(frame["topic"], target_camera):
continue
if not matched_topic:
matched_topic = str(frame["topic"])
selected.append(frame)
if not selected:
raise FileNotFoundError(f"no MCAP frames found for camera selector {target_camera}")
return selected, matched_topic
def find_video_frame_match(index_payload: Optional[dict[str, Any]], target_frame_id: int) -> tuple[int | None, str | None]:
if not index_payload:
return None, None
fields = index_payload.get("fields", {}) or {}
index_list = index_payload.get("index", []) or []
for field_name in ("frame_id", "cve_frame_id"):
if field_name not in fields:
continue
for frame_idx in range(len(index_list)):
info = get_video_frame_info(index_payload, frame_idx)
if info is None:
continue
value = info.get(field_name)
try:
if int(value) == target_frame_id:
return frame_idx, field_name
except (TypeError, ValueError):
continue
return None, None
def collect_video_window(
video_path: Path,
target_frame_id: int,
window_radius: int,
) -> tuple[list[dict[str, Any]], str, int]:
index_payload = read_video_frame_index(video_path)
matched_frame_idx, matched_field = find_video_frame_match(index_payload, target_frame_id)
if matched_frame_idx is None or matched_field is None:
raise FileNotFoundError(f"target frame_id {target_frame_id} not found in video index")
start_idx = max(0, matched_frame_idx - window_radius)
end_idx = matched_frame_idx + window_radius
cap = cv2.VideoCapture(str(video_path))
if not cap.isOpened():
raise RuntimeError(f"failed to open video file: {video_path}")
selected: list[dict[str, Any]] = []
try:
cap.set(cv2.CAP_PROP_POS_FRAMES, start_idx)
current_idx = start_idx
while current_idx <= end_idx:
ret, frame = cap.read()
if not ret:
break
frame_info = get_video_frame_info(index_payload, current_idx) if index_payload else None
selected.append(
{
"source_path": video_path,
"frame_idx": current_idx,
"frame_id": None if frame_info is None else frame_info.get("frame_id"),
"cve_frame_id": None if frame_info is None else frame_info.get("cve_frame_id"),
"timestamp": "" if frame_info is None else str(frame_info.get("timestamp", "")),
"image": frame,
}
)
current_idx += 1
finally:
cap.release()
return selected, matched_field, matched_frame_idx
def collect_video_all_frames(video_path: Path) -> tuple[list[dict[str, Any]], int]:
index_payload = read_video_frame_index(video_path)
cap = cv2.VideoCapture(str(video_path))
if not cap.isOpened():
raise RuntimeError(f"failed to open video file: {video_path}")
selected: list[dict[str, Any]] = []
current_idx = 0
try:
while True:
ret, frame = cap.read()
if not ret:
break
frame_info = get_video_frame_info(index_payload, current_idx) if index_payload else None
selected.append(
{
"source_path": video_path,
"frame_idx": current_idx,
"frame_id": None if frame_info is None else frame_info.get("frame_id"),
"cve_frame_id": None if frame_info is None else frame_info.get("cve_frame_id"),
"timestamp": "" if frame_info is None else str(frame_info.get("timestamp", "")),
"image": frame,
}
)
current_idx += 1
finally:
cap.release()
if not selected:
raise FileNotFoundError(f"no readable frames found in video file: {video_path}")
return selected, len(selected)
def save_decoded_frames(
issue_id: int,
output_dir: Path,
frames: list[dict[str, Any]],
target_frame_id: int,
window_radius: int,
jpg_quality: int,
dry_run: bool,
decode_all: bool,
) -> int:
images_dir = output_dir / ("frames_all" if decode_all else "frames_window")
if dry_run:
return len(frames)
ensure_dir(images_dir, dry_run=False)
encode_params = [int(cv2.IMWRITE_JPEG_QUALITY), int(jpg_quality)]
target_index = None if decode_all else min(window_radius, len(frames) - 1)
for index, frame in enumerate(frames):
offset = 0 if target_index is None else index - target_index
frame_id = frame.get("frame_id")
if frame_id is None:
frame_id = frame.get("cve_frame_id")
frame_id_token = "na" if frame_id is None else str(frame_id)
topic = str(frame.get("topic") or "")
camera_id_match = re.search(r"camera(\d+)", topic, re.IGNORECASE)
if camera_id_match:
camera_id_token = f"camera{camera_id_match.group(1)}"
else:
source_path = Path(str(frame.get("source_path") or ""))
camera_id_token = source_path.stem if source_path.stem else "camera"
if decode_all:
filename = f"{issue_id}_{camera_id_token}_{frame_id_token}.jpg"
else:
filename = f"{issue_id}_{camera_id_token}_{frame_id_token}.jpg"
image = frame.get("image")
if image is None:
image = _h265_payload_to_bgr(frame["payload"])
if not cv2.imwrite(str(images_dir / filename), image, encode_params):
raise IOError(f"failed to write image: {images_dir / filename}")
return len(frames)
def process_case(case: CaseSource, output_root: Path, window_radius: int, jpg_quality: int, dry_run: bool, skip_existing: bool) -> CaseResult:
output_dir = output_root / case.relative_case_dir
images_dir = output_dir / ("frames_all" if case.decode_all else "frames_window")
if skip_existing and images_dir.is_dir():
existing_images = [path for path in images_dir.iterdir() if path.is_file()]
if existing_images:
return CaseResult(
issue_id=case.issue_id,
issue_name=case.issue_name,
case_dir=str(case.case_dir),
relative_case_dir=str(case.relative_case_dir),
target_camera=case.target_frame.camera,
target_frame_id=case.target_frame.frame_id,
decode_mode="all" if case.decode_all else "window",
source_mode=case.source_mode,
source_paths=[str(path) for path in case.source_paths],
output_dir=str(output_dir),
status="skipped_existing",
detail="existing extracted frames found",
extracted_count=len(existing_images),
)
try:
if case.source_mode == "mcap_stream":
try:
if case.decode_all:
frames, matched_topic = collect_mcap_all_frames(case.source_paths, case.target_frame.camera)
matched_field = None
else:
frames, matched_field, matched_topic = collect_mcap_window(
case.source_paths,
case.target_frame.camera,
case.target_frame.frame_id,
window_radius,
)
extracted_count = save_decoded_frames(
case.issue_id, output_dir, frames, case.target_frame.frame_id, window_radius, jpg_quality, dry_run, case.decode_all
)
detail = (
f"decoded {extracted_count} frames from {len(case.source_paths)} mcap file(s)"
if case.decode_all
else f"decoded {extracted_count} frames from {len(case.source_paths)} mcap file(s)"
)
return CaseResult(
issue_id=case.issue_id,
issue_name=case.issue_name,
case_dir=str(case.case_dir),
relative_case_dir=str(case.relative_case_dir),
target_camera=case.target_frame.camera,
target_frame_id=case.target_frame.frame_id,
decode_mode="all" if case.decode_all else "window",
source_mode=case.source_mode,
source_paths=[str(path) for path in case.source_paths],
output_dir=str(output_dir),
status="planned" if dry_run else "decoded_mcap",
detail=("would " + detail) if dry_run else detail,
matched_field=matched_field,
matched_frame_idx=None,
matched_topic=matched_topic,
extracted_count=extracted_count,
)
except Exception as mcap_exc:
fallback_camera4 = find_camera4_bin(case.case_dir)
if fallback_camera4 is None or case.target_frame.camera not in {"camera4", "any"}:
raise mcap_exc
if case.decode_all:
frames, extracted_count_all = collect_video_all_frames(fallback_camera4)
matched_field = None
matched_frame_idx = None
extracted_count = save_decoded_frames(
case.issue_id, output_dir, frames, case.target_frame.frame_id, window_radius, jpg_quality, dry_run, case.decode_all
)
else:
frames, matched_field, matched_frame_idx = collect_video_window(
fallback_camera4,
case.target_frame.frame_id,
window_radius,
)
extracted_count = save_decoded_frames(
case.issue_id, output_dir, frames, case.target_frame.frame_id, window_radius, jpg_quality, dry_run, case.decode_all
)
detail = (
f"decoded {extracted_count} frames from camera4.bin fallback after mcap lookup failed: "
f"{type(mcap_exc).__name__}: {mcap_exc}"
)
return CaseResult(
issue_id=case.issue_id,
issue_name=case.issue_name,
case_dir=str(case.case_dir),
relative_case_dir=str(case.relative_case_dir),
target_camera=case.target_frame.camera,
target_frame_id=case.target_frame.frame_id,
decode_mode="all" if case.decode_all else "window",
source_mode="camera4_bin_fallback",
source_paths=[str(path) for path in case.source_paths] + [str(fallback_camera4)],
output_dir=str(output_dir),
status="planned" if dry_run else "decoded_camera4_bin_fallback",
detail=("would " + detail) if dry_run else detail,
matched_field=matched_field,
matched_frame_idx=matched_frame_idx,
matched_topic=None,
extracted_count=extracted_count,
)
if case.source_mode == "camera4_bin":
if case.decode_all:
frames, _ = collect_video_all_frames(case.source_paths[0])
matched_field, matched_frame_idx = None, None
else:
frames, matched_field, matched_frame_idx = collect_video_window(case.source_paths[0], case.target_frame.frame_id, window_radius)
extracted_count = save_decoded_frames(
case.issue_id, output_dir, frames, case.target_frame.frame_id, window_radius, jpg_quality, dry_run, case.decode_all
)
detail = f"decoded {extracted_count} frames from camera4.bin"
return CaseResult(
issue_id=case.issue_id,
issue_name=case.issue_name,
case_dir=str(case.case_dir),
relative_case_dir=str(case.relative_case_dir),
target_camera=case.target_frame.camera,
target_frame_id=case.target_frame.frame_id,
decode_mode="all" if case.decode_all else "window",
source_mode=case.source_mode,
source_paths=[str(path) for path in case.source_paths],
output_dir=str(output_dir),
status="planned" if dry_run else "decoded_camera4_bin",
detail=("would " + detail) if dry_run else detail,
matched_field=matched_field,
matched_frame_idx=matched_frame_idx,
matched_topic=None,
extracted_count=extracted_count,
)
return CaseResult(
issue_id=case.issue_id,
issue_name=case.issue_name,
case_dir=str(case.case_dir),
relative_case_dir=str(case.relative_case_dir),
target_camera=case.target_frame.camera,
target_frame_id=case.target_frame.frame_id,
decode_mode="all" if case.decode_all else "window",
source_mode=case.source_mode,
source_paths=[str(path) for path in case.source_paths],
output_dir=str(output_dir),
status="skipped_no_source",
detail=f"unsupported source mode: {case.source_mode}",
)
except Exception as exc:
return CaseResult(
issue_id=case.issue_id,
issue_name=case.issue_name,
case_dir=str(case.case_dir),
relative_case_dir=str(case.relative_case_dir),
target_camera=case.target_frame.camera,
target_frame_id=case.target_frame.frame_id,
decode_mode="all" if case.decode_all else "window",
source_mode=case.source_mode,
source_paths=[str(path) for path in case.source_paths],
output_dir=str(output_dir),
status="failed",
detail=f"{type(exc).__name__}: {exc}",
)
def build_manifest(args: argparse.Namespace, input_json: Path, download_root: Path, output_root: Path, discovered: list[CaseSource], results: list[CaseResult]) -> dict[str, Any]:
summary = Counter(result.status for result in results)
return {
"generated_at": datetime.now().astimezone().isoformat(timespec="seconds"),
"input_json": str(input_json),
"download_root": str(download_root),
"output_root": str(output_root),
"issue_filter": args.issue_ids or [],
"decode_all_issue_filter": args.decode_all_issue_ids or [],
"window_radius": args.window_radius,
"window_size": args.window_radius * 2 + 1,
"jpg_quality": args.jpg_quality,
"dry_run": args.dry_run,
"skip_existing": args.skip_existing,
"total_discovered_cases": len(discovered),
"summary": dict(summary),
"cases": [result.to_dict() for result in results],
}
def print_summary(manifest: dict[str, Any]) -> None:
print(f"input_json: {manifest['input_json']}")
print(f"download_root: {manifest['download_root']}")
print(f"output_root: {manifest['output_root']}")
print(f"window_size: {manifest['window_size']}")
print(f"dry_run: {manifest['dry_run']}")
print(f"total_discovered_cases: {manifest['total_discovered_cases']}")
for status, count in sorted(manifest["summary"].items()):
print(f"{status}: {count}")
def main() -> int:
args = parse_args()
input_json = Path(args.input_json).resolve()
download_root = Path(args.download_root).resolve()
output_root = Path(args.output_root).resolve()
manifest_path = (
Path(args.manifest_path).resolve()
if args.manifest_path
else output_root / "decode_manifest.json"
)
items = load_issue_items(input_json)
issue_filter = set(args.issue_ids) if args.issue_ids else None
decode_all_issue_filter = set(args.decode_all_issue_ids or [])
discovered, skipped = discover_case_sources(items, download_root, issue_filter, decode_all_issue_filter)
results = list(skipped)
for case in discovered:
results.append(process_case(case, output_root, args.window_radius, args.jpg_quality, args.dry_run, args.skip_existing))
manifest = build_manifest(args, input_json, download_root, output_root, discovered, results)
if not args.dry_run:
ensure_dir(manifest_path.parent, dry_run=False)
manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print_summary(manifest)
if args.dry_run:
print(f"manifest (not written in dry-run): {manifest_path}")
else:
print(f"manifest: {manifest_path}")
return 0
if __name__ == "__main__":
sys.exit(main())