单目3D初始代码
This commit is contained in:
12
tools/temporal_analysis/add_sub-cls_ts_in_tracking.md
Executable file
12
tools/temporal_analysis/add_sub-cls_ts_in_tracking.md
Executable file
@@ -0,0 +1,12 @@
|
||||
tools/temporal_analysis/track_objects.py
|
||||
上述脚本对检测结果进行跟踪处理,现在期望对每个检测结果补充两个字段。
|
||||
|
||||
示例结果路径:/data1/dongying/Mono3d/G1M3/cases_regular/model_20260317_with_cls/019b2a13-9af3-7e97-a109-9fb7ba7a8f22
|
||||
其中,merge_json中新增了"attribute"字段,其中,"attr_cls"数值表示具体类别。同时路径中有frame_timestamps_interp.csv文件记录了时间戳信息。
|
||||
|
||||
第一个字段是细分类别(sub_cls),具体说明如下:
|
||||
1. 只针对车辆和行人两个大类目标定义细分类别
|
||||
2. 对于车辆类别,当"attr_cls"数值<=11时,自动填入; 当"attr_cls"数值>11,且不等于23时,细分类别数值=int("attr_cls")+3; 当int("attr_cls")=23时,细分类别数值=12;当"is_fake"为1时,细分类别数值=26
|
||||
3. 对于行人类别,细分类别数值=int("attr_cls")
|
||||
|
||||
第二个字段是时间戳信息,需根据merge_json/roi0/roi1中文件名最后的frameid信息,查找frame_timestamps_interp.csv文件中"camera4_frame_id"列对应数值的索引,然后获取"camera4"列中对应索引的时间戳信息。
|
||||
1156
tools/temporal_analysis/analyze_heading_debug.py
Executable file
1156
tools/temporal_analysis/analyze_heading_debug.py
Executable file
File diff suppressed because it is too large
Load Diff
437
tools/temporal_analysis/analyze_tracking_loss.py
Executable file
437
tools/temporal_analysis/analyze_tracking_loss.py
Executable file
@@ -0,0 +1,437 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Analyze whether detections are lost between raw per-frame JSONs and tracking output.
|
||||
|
||||
Primary use case:
|
||||
compare {case_dir}/roi0/*.json against {case_dir}/roi0.json
|
||||
|
||||
The script reports:
|
||||
- frame-level coverage differences
|
||||
- total / per-class detection count deltas
|
||||
- per-frame missing/new detection counts
|
||||
- detailed missing/new samples for debugging
|
||||
|
||||
Usage:
|
||||
python tools/temporal_analysis/analyze_tracking_loss.py \
|
||||
--case-dir /path/to/case --source roi0
|
||||
|
||||
python tools/temporal_analysis/analyze_tracking_loss.py \
|
||||
--raw-dir /path/to/roi0 \
|
||||
--tracking /path/to/roi0.json \
|
||||
--output /path/to/roi0_loss_report.json
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from collections import Counter, defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
from merge_tracking_results import normalize_image_name
|
||||
|
||||
|
||||
def round_float(value, digits=6):
|
||||
"""Round float-like values to stabilize comparison keys."""
|
||||
return round(float(value), digits)
|
||||
|
||||
|
||||
def normalize_bbox(bbox):
|
||||
"""Convert bbox to a rounded tuple."""
|
||||
return tuple(round_float(v) for v in bbox[:4])
|
||||
|
||||
|
||||
def normalize_vector(values, limit=None):
|
||||
"""Convert an optional numeric vector to a rounded tuple."""
|
||||
if values is None:
|
||||
return None
|
||||
seq = values if limit is None else values[:limit]
|
||||
return tuple(round_float(v) for v in seq)
|
||||
|
||||
|
||||
def detection_signature(det):
|
||||
"""Build a stable comparison signature for one detection."""
|
||||
return (
|
||||
int(det.get("class_id", -1)),
|
||||
normalize_bbox(det.get("bbox", [0, 0, 0, 0])),
|
||||
round_float(det.get("confidence", 0.0)),
|
||||
str(det.get("type_name", "")),
|
||||
str(det.get("face_cls", "")),
|
||||
int(det.get("cut_cls", -1)),
|
||||
str(det.get("cut_cls_name", "")),
|
||||
str(det.get("anchor", "")),
|
||||
normalize_vector(det.get("object_3d"), limit=7),
|
||||
normalize_vector(det.get("object_3d_ego"), limit=7),
|
||||
)
|
||||
|
||||
|
||||
def serialize_detection(det):
|
||||
"""Return a compact JSON-friendly detection summary."""
|
||||
data = {
|
||||
"class_id": det.get("class_id"),
|
||||
"bbox": det.get("bbox"),
|
||||
"confidence": det.get("confidence"),
|
||||
}
|
||||
optional_keys = [
|
||||
"track_id",
|
||||
"type_name",
|
||||
"face_cls",
|
||||
"cut_cls",
|
||||
"cut_cls_name",
|
||||
"anchor",
|
||||
"frameId",
|
||||
]
|
||||
for key in optional_keys:
|
||||
if key in det:
|
||||
data[key] = det.get(key)
|
||||
return data
|
||||
|
||||
|
||||
def parse_det_format(det_dict, image_name=None):
|
||||
"""Parse raw single-frame detection JSON into the tracking input schema."""
|
||||
if "detections" in det_dict and isinstance(det_dict["detections"], dict):
|
||||
raw_detections = det_dict["detections"]
|
||||
else:
|
||||
raw_detections = det_dict
|
||||
|
||||
face_map = {
|
||||
"front": "kMonocular3DFront",
|
||||
"tail": "kMonocular3DRear",
|
||||
"back": "kMonocular3DRear",
|
||||
"left": "kMonocular3DLeft",
|
||||
"right": "kMonocular3DRight",
|
||||
"center": "kMonocular3DCenter",
|
||||
"none": "kMonocular3DCenter",
|
||||
}
|
||||
|
||||
detections = []
|
||||
for det in raw_detections.values():
|
||||
class_id = int(det["type"])
|
||||
bbox = [float(v) for v in det["box2d"]]
|
||||
score = float(det["score"])
|
||||
|
||||
xyzlhwyaw_raw = det.get("xyzlhwyaw", [])
|
||||
object_3d = None
|
||||
if xyzlhwyaw_raw and float(xyzlhwyaw_raw[0]) != -1:
|
||||
object_3d = [float(v) for v in xyzlhwyaw_raw]
|
||||
|
||||
xyzlhwyaw_ego_raw = det.get("xyzlhwyaw_ego", [])
|
||||
object_3d_ego = None
|
||||
if xyzlhwyaw_ego_raw and float(xyzlhwyaw_ego_raw[0]) != -1:
|
||||
object_3d_ego = [float(v) for v in xyzlhwyaw_ego_raw]
|
||||
|
||||
detection = {
|
||||
"bbox": bbox,
|
||||
"confidence": score,
|
||||
"class_id": class_id,
|
||||
"type_name": det.get("type_name", ""),
|
||||
"face_cls": det.get("face_cls", "none"),
|
||||
"cut_cls": int(det.get("cut_cls", -1)),
|
||||
"cut_cls_name": det.get("cut_cls_name", "none"),
|
||||
"frameId": normalize_image_name(image_name).split("_")[-1] if image_name else None,
|
||||
"version": "20260228",
|
||||
"timestamp": 0,
|
||||
}
|
||||
detection["anchor"] = face_map.get(detection["face_cls"], "kMonocular3DCenter")
|
||||
|
||||
if object_3d is not None:
|
||||
detection["object_3d"] = object_3d
|
||||
if object_3d_ego is not None:
|
||||
detection["object_3d_ego"] = object_3d_ego
|
||||
|
||||
detections.append(detection)
|
||||
|
||||
return {
|
||||
"image_name": image_name,
|
||||
"detections": detections,
|
||||
}
|
||||
|
||||
|
||||
def load_predictions_from_dir(input_dir, pattern="*.json"):
|
||||
"""Load raw per-frame JSON files using the same schema as tracking input."""
|
||||
input_dir = Path(input_dir)
|
||||
json_files = sorted(input_dir.glob(pattern))
|
||||
predictions_data = []
|
||||
for json_file in json_files:
|
||||
with open(json_file, "r", encoding="utf-8") as f:
|
||||
det_dict = json.load(f)
|
||||
predictions_data.append(parse_det_format(det_dict, image_name=json_file.stem))
|
||||
return predictions_data
|
||||
|
||||
|
||||
def load_tracking_frames(tracking_path):
|
||||
"""Load tracking frames from track_objects.py output."""
|
||||
with open(tracking_path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
if not isinstance(data, list):
|
||||
raise ValueError(f"Expected a list of frames in {tracking_path}, got {type(data).__name__}")
|
||||
return data
|
||||
|
||||
|
||||
def index_frames(frames):
|
||||
"""Index frames by normalized image name."""
|
||||
indexed = {}
|
||||
ordered_keys = []
|
||||
for frame in frames:
|
||||
raw_name = frame.get("image_name") or ""
|
||||
key = normalize_image_name(Path(raw_name).stem if raw_name else "")
|
||||
indexed[key] = frame
|
||||
ordered_keys.append(key)
|
||||
return indexed, ordered_keys
|
||||
|
||||
|
||||
def counter_to_detections(detections):
|
||||
"""Build a multiset of detection signatures."""
|
||||
counter = Counter()
|
||||
det_by_sig = defaultdict(list)
|
||||
for det in detections:
|
||||
sig = detection_signature(det)
|
||||
counter[sig] += 1
|
||||
det_by_sig[sig].append(det)
|
||||
return counter, det_by_sig
|
||||
|
||||
|
||||
def expand_counter_delta(delta_counter, det_by_sig):
|
||||
"""Materialize counter deltas back into example detections."""
|
||||
items = []
|
||||
for sig, count in delta_counter.items():
|
||||
for idx in range(count):
|
||||
items.append(serialize_detection(det_by_sig[sig][idx]))
|
||||
return items
|
||||
|
||||
|
||||
def compute_class_counts(detections):
|
||||
"""Count detections by class_id."""
|
||||
counts = Counter()
|
||||
for det in detections:
|
||||
counts[int(det.get("class_id", -1))] += 1
|
||||
return counts
|
||||
|
||||
|
||||
def analyze_pair(raw_frames, tracking_frames, top_k_frames=20, top_k_samples=200):
|
||||
"""Compare raw parsed frames with tracking output frames."""
|
||||
raw_index, raw_order = index_frames(raw_frames)
|
||||
tracking_index, tracking_order = index_frames(tracking_frames)
|
||||
all_keys = []
|
||||
seen = set()
|
||||
for key in raw_order + tracking_order:
|
||||
if key not in seen:
|
||||
all_keys.append(key)
|
||||
seen.add(key)
|
||||
|
||||
frame_reports = []
|
||||
totals = {
|
||||
"raw_frames": len(raw_frames),
|
||||
"tracking_frames": len(tracking_frames),
|
||||
"shared_frames": 0,
|
||||
"raw_only_frames": 0,
|
||||
"tracking_only_frames": 0,
|
||||
"raw_detections": 0,
|
||||
"tracking_detections": 0,
|
||||
"matched_detections": 0,
|
||||
"missing_detections": 0,
|
||||
"new_detections": 0,
|
||||
}
|
||||
per_class = defaultdict(lambda: {"raw": 0, "tracking": 0, "missing": 0, "new": 0})
|
||||
missing_samples = []
|
||||
new_samples = []
|
||||
|
||||
for key in all_keys:
|
||||
raw_frame = raw_index.get(key)
|
||||
tracking_frame = tracking_index.get(key)
|
||||
raw_dets = raw_frame.get("detections", []) if raw_frame else []
|
||||
tracking_dets = tracking_frame.get("detections", []) if tracking_frame else []
|
||||
|
||||
if raw_frame and tracking_frame:
|
||||
totals["shared_frames"] += 1
|
||||
elif raw_frame:
|
||||
totals["raw_only_frames"] += 1
|
||||
else:
|
||||
totals["tracking_only_frames"] += 1
|
||||
|
||||
raw_count = len(raw_dets)
|
||||
tracking_count = len(tracking_dets)
|
||||
totals["raw_detections"] += raw_count
|
||||
totals["tracking_detections"] += tracking_count
|
||||
|
||||
raw_by_class = compute_class_counts(raw_dets)
|
||||
tracking_by_class = compute_class_counts(tracking_dets)
|
||||
for cls_id, count in raw_by_class.items():
|
||||
per_class[cls_id]["raw"] += count
|
||||
for cls_id, count in tracking_by_class.items():
|
||||
per_class[cls_id]["tracking"] += count
|
||||
|
||||
raw_counter, raw_det_by_sig = counter_to_detections(raw_dets)
|
||||
tracking_counter, tracking_det_by_sig = counter_to_detections(tracking_dets)
|
||||
matched_counter = raw_counter & tracking_counter
|
||||
missing_counter = raw_counter - tracking_counter
|
||||
new_counter = tracking_counter - raw_counter
|
||||
|
||||
matched_count = sum(matched_counter.values())
|
||||
missing_count = sum(missing_counter.values())
|
||||
new_count = sum(new_counter.values())
|
||||
|
||||
totals["matched_detections"] += matched_count
|
||||
totals["missing_detections"] += missing_count
|
||||
totals["new_detections"] += new_count
|
||||
|
||||
missing_examples = expand_counter_delta(missing_counter, raw_det_by_sig)
|
||||
new_examples = expand_counter_delta(new_counter, tracking_det_by_sig)
|
||||
|
||||
for det in missing_examples:
|
||||
cls_id = int(det.get("class_id", -1))
|
||||
per_class[cls_id]["missing"] += 1
|
||||
for det in new_examples:
|
||||
cls_id = int(det.get("class_id", -1))
|
||||
per_class[cls_id]["new"] += 1
|
||||
|
||||
frame_report = {
|
||||
"image_name": key,
|
||||
"raw_present": raw_frame is not None,
|
||||
"tracking_present": tracking_frame is not None,
|
||||
"raw_count": raw_count,
|
||||
"tracking_count": tracking_count,
|
||||
"matched_count": matched_count,
|
||||
"missing_count": missing_count,
|
||||
"new_count": new_count,
|
||||
}
|
||||
|
||||
if missing_examples:
|
||||
for det in missing_examples[: max(0, top_k_samples - len(missing_samples))]:
|
||||
missing_samples.append({"image_name": key, "detection": det})
|
||||
if new_examples:
|
||||
for det in new_examples[: max(0, top_k_samples - len(new_samples))]:
|
||||
new_samples.append({"image_name": key, "detection": det})
|
||||
|
||||
frame_reports.append(frame_report)
|
||||
|
||||
frame_reports_sorted = sorted(
|
||||
frame_reports,
|
||||
key=lambda item: (item["missing_count"], item["new_count"], abs(item["raw_count"] - item["tracking_count"])),
|
||||
reverse=True,
|
||||
)
|
||||
per_class_report = {
|
||||
str(cls_id): values
|
||||
for cls_id, values in sorted(per_class.items(), key=lambda item: item[0])
|
||||
}
|
||||
|
||||
loss_rate = (
|
||||
totals["missing_detections"] / totals["raw_detections"]
|
||||
if totals["raw_detections"] > 0 else 0.0
|
||||
)
|
||||
new_rate = (
|
||||
totals["new_detections"] / totals["tracking_detections"]
|
||||
if totals["tracking_detections"] > 0 else 0.0
|
||||
)
|
||||
|
||||
return {
|
||||
"summary": {
|
||||
**totals,
|
||||
"loss_rate_vs_raw": loss_rate,
|
||||
"new_rate_vs_tracking": new_rate,
|
||||
},
|
||||
"per_class": per_class_report,
|
||||
"top_frames_by_diff": frame_reports_sorted[:top_k_frames],
|
||||
"missing_samples": missing_samples,
|
||||
"new_samples": new_samples,
|
||||
"all_frame_reports": frame_reports,
|
||||
}
|
||||
|
||||
|
||||
def resolve_inputs(case_dir, source, raw_dir, tracking_path):
|
||||
"""Resolve raw/tracking inputs from CLI arguments."""
|
||||
if case_dir is not None:
|
||||
case_dir = Path(case_dir)
|
||||
resolved_raw = case_dir / source
|
||||
resolved_tracking = case_dir / f"{source}.json"
|
||||
else:
|
||||
resolved_raw = Path(raw_dir) if raw_dir is not None else None
|
||||
resolved_tracking = Path(tracking_path) if tracking_path is not None else None
|
||||
|
||||
if resolved_raw is None or resolved_tracking is None:
|
||||
raise ValueError("Provide either --case-dir or both --raw-dir and --tracking.")
|
||||
if not resolved_raw.is_dir():
|
||||
raise FileNotFoundError(f"Raw input directory not found: {resolved_raw}")
|
||||
if not resolved_tracking.is_file():
|
||||
raise FileNotFoundError(f"Tracking JSON not found: {resolved_tracking}")
|
||||
return resolved_raw, resolved_tracking
|
||||
|
||||
|
||||
def print_report(report, raw_dir, tracking_path, source):
|
||||
"""Pretty-print the key report numbers."""
|
||||
summary = report["summary"]
|
||||
print("")
|
||||
print("======================================================================")
|
||||
print(f"Tracking loss analysis for source: {source}")
|
||||
print("======================================================================")
|
||||
print(f"Raw directory : {raw_dir}")
|
||||
print(f"Tracking JSON : {tracking_path}")
|
||||
print(f"Raw frames : {summary['raw_frames']}")
|
||||
print(f"Tracking frames : {summary['tracking_frames']}")
|
||||
print(f"Shared frames : {summary['shared_frames']}")
|
||||
print(f"Raw-only frames : {summary['raw_only_frames']}")
|
||||
print(f"Track-only frames: {summary['tracking_only_frames']}")
|
||||
print(f"Raw detections : {summary['raw_detections']}")
|
||||
print(f"Track detections: {summary['tracking_detections']}")
|
||||
print(f"Matched : {summary['matched_detections']}")
|
||||
print(f"Missing : {summary['missing_detections']} ({summary['loss_rate_vs_raw']:.2%} of raw)")
|
||||
print(f"New : {summary['new_detections']} ({summary['new_rate_vs_tracking']:.2%} of tracking)")
|
||||
|
||||
print("\nPer-class summary:")
|
||||
for cls_id, stats in report["per_class"].items():
|
||||
print(
|
||||
f" class {cls_id}: raw={stats['raw']}, tracking={stats['tracking']}, "
|
||||
f"missing={stats['missing']}, new={stats['new']}"
|
||||
)
|
||||
|
||||
print("\nTop frames by difference:")
|
||||
if not report["top_frames_by_diff"]:
|
||||
print(" (no frames)")
|
||||
for item in report["top_frames_by_diff"]:
|
||||
print(
|
||||
f" {item['image_name']}: raw={item['raw_count']}, tracking={item['tracking_count']}, "
|
||||
f"missing={item['missing_count']}, new={item['new_count']}"
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Analyze whether detections are lost from raw per-frame JSONs to tracking JSON."
|
||||
)
|
||||
parser.add_argument("--case-dir", type=str, default=None,
|
||||
help="Case directory containing <source>/ and <source>.json")
|
||||
parser.add_argument("--source", type=str, default="roi0",
|
||||
help="Source name under case-dir, e.g. roi0 / roi1 / merge")
|
||||
parser.add_argument("--raw-dir", type=str, default=None,
|
||||
help="Raw per-frame JSON directory, used when --case-dir is omitted")
|
||||
parser.add_argument("--tracking", type=str, default=None,
|
||||
help="Tracking JSON path, used when --case-dir is omitted")
|
||||
parser.add_argument("--file-pattern", type=str, default="*.json",
|
||||
help="Glob pattern for raw frame JSON files (default: %(default)s)")
|
||||
parser.add_argument("--output", type=str, default=None,
|
||||
help="Optional JSON report output path")
|
||||
parser.add_argument("--top-k-frames", type=int, default=20,
|
||||
help="Number of most-different frames to include in the summary (default: %(default)s)")
|
||||
parser.add_argument("--top-k-samples", type=int, default=200,
|
||||
help="Maximum missing/new samples to store in the JSON report (default: %(default)s)")
|
||||
args = parser.parse_args()
|
||||
|
||||
raw_dir, tracking_path = resolve_inputs(args.case_dir, args.source, args.raw_dir, args.tracking)
|
||||
raw_frames = load_predictions_from_dir(raw_dir, pattern=args.file_pattern)
|
||||
tracking_frames = load_tracking_frames(tracking_path)
|
||||
|
||||
report = analyze_pair(
|
||||
raw_frames=raw_frames,
|
||||
tracking_frames=tracking_frames,
|
||||
top_k_frames=args.top_k_frames,
|
||||
top_k_samples=args.top_k_samples,
|
||||
)
|
||||
print_report(report, raw_dir, tracking_path, args.source)
|
||||
|
||||
if args.output:
|
||||
output_path = Path(args.output)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
json.dump(report, f, indent=2, ensure_ascii=False)
|
||||
print(f"\nReport written to: {output_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
118
tools/temporal_analysis/analyze_tracking_loss.sh
Executable file
118
tools/temporal_analysis/analyze_tracking_loss.sh
Executable file
@@ -0,0 +1,118 @@
|
||||
#!/bin/bash
|
||||
# 用法说明:
|
||||
# 分析原始逐帧检测结果与跟踪结果之间是否存在检测框丢失。
|
||||
#
|
||||
# 默认对每个 case 的三路输入分别分析:
|
||||
# ROI0: {case_dir}/roi0/ ↔ {case_dir}/roi0.json
|
||||
# ROI1: {case_dir}/roi1/ ↔ {case_dir}/roi1.json
|
||||
# Merge: {case_dir}/merge_json/ ↔ {case_dir}/merge.json
|
||||
#
|
||||
# 输出报告:
|
||||
# {case_dir}/roi0_loss_report.json
|
||||
# {case_dir}/roi1_loss_report.json
|
||||
# {case_dir}/merge_loss_report.json
|
||||
#
|
||||
# 支持两种模式:
|
||||
# 1) 批量模式(默认):处理 RESULTS_ROOT/MODEL_NAME 下所有 case
|
||||
# bash analyze_tracking_loss.sh
|
||||
# 2) 单 case 模式:指定 case 目录路径作为第一个参数
|
||||
# bash analyze_tracking_loss.sh /path/to/case
|
||||
|
||||
set -u
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
export PYTHONPATH="${PROJECT_ROOT}:${PYTHONPATH:-}"
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 配置区:与 track_objects.sh 保持一致
|
||||
# -----------------------------------------------------------------------
|
||||
RESULTS_ROOT="/data1/dongying/Mono3d/G1M3/cases_regular"
|
||||
MODEL_NAME="model_20260228"
|
||||
|
||||
TOP_K_FRAMES=20
|
||||
TOP_K_SAMPLES=200
|
||||
RAW_FILE_PATTERN="*.json"
|
||||
|
||||
analyze_one_source() {
|
||||
local case_dir="$1"
|
||||
local raw_subdir="$2"
|
||||
local tracking_name="$3"
|
||||
local report_name="$4"
|
||||
local source_label="$5"
|
||||
|
||||
local raw_dir="${case_dir}/${raw_subdir}"
|
||||
local tracking_json="${case_dir}/${tracking_name}"
|
||||
local report_json="${case_dir}/${report_name}"
|
||||
|
||||
if [[ ! -d "${raw_dir}" ]]; then
|
||||
echo "Warning: raw directory not found, skipping ${source_label}: ${raw_dir}"
|
||||
return 0
|
||||
fi
|
||||
if [[ ! -f "${tracking_json}" ]]; then
|
||||
echo "Warning: tracking json not found, skipping ${source_label}: ${tracking_json}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- Analyze ${source_label} ---"
|
||||
python3 "${PROJECT_ROOT}/tools/temporal_analysis/analyze_tracking_loss.py" \
|
||||
--raw-dir "${raw_dir}" \
|
||||
--tracking "${tracking_json}" \
|
||||
--output "${report_json}" \
|
||||
--file-pattern "${RAW_FILE_PATTERN}" \
|
||||
--top-k-frames "${TOP_K_FRAMES}" \
|
||||
--top-k-samples "${TOP_K_SAMPLES}"
|
||||
}
|
||||
|
||||
analyze_one_case() {
|
||||
local case_dir="$1"
|
||||
|
||||
if [[ ! -d "${case_dir}" ]]; then
|
||||
echo "Error: case directory does not exist: ${case_dir}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "======================================================================"
|
||||
echo "Analyze tracking loss: ${case_dir}"
|
||||
echo "======================================================================"
|
||||
|
||||
analyze_one_source "${case_dir}" "roi0" "roi0.json" "roi0_loss_report.json" "roi0"
|
||||
analyze_one_source "${case_dir}" "roi1" "roi1.json" "roi1_loss_report.json" "roi1"
|
||||
analyze_one_source "${case_dir}" "merge_json" "merge.json" "merge_loss_report.json" "merge"
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "######################################################################"
|
||||
echo "# Analyze detection loss between raw per-frame JSON and tracking JSON"
|
||||
echo "######################################################################"
|
||||
|
||||
if [[ -n "${1:-}" ]]; then
|
||||
CASE_DIR="$1"
|
||||
echo "Single-case mode: ${CASE_DIR}"
|
||||
analyze_one_case "${CASE_DIR}"
|
||||
exit $?
|
||||
fi
|
||||
|
||||
MODEL_DIR="${RESULTS_ROOT}/${MODEL_NAME}"
|
||||
if [[ ! -d "${MODEL_DIR}" ]]; then
|
||||
echo "Error: model directory not found: ${MODEL_DIR}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mapfile -t CASE_DIRS < <(find "${MODEL_DIR}" -mindepth 1 -maxdepth 1 -type d | sort)
|
||||
|
||||
if [[ ${#CASE_DIRS[@]} -eq 0 ]]; then
|
||||
echo "Warning: no case directories found under ${MODEL_DIR}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Batch mode: found ${#CASE_DIRS[@]} case(s) under ${MODEL_DIR}"
|
||||
|
||||
for case_dir in "${CASE_DIRS[@]}"; do
|
||||
analyze_one_case "${case_dir}" || exit 1
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Tracking loss analysis complete."
|
||||
44
tools/temporal_analysis/compare_temporal.sh
Executable file
44
tools/temporal_analysis/compare_temporal.sh
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Example usage of compare_temporal_stability.py
|
||||
# Compare temporal stability between two models
|
||||
|
||||
|
||||
# 019b18ef-ca2b-7e97-8ca5-d4b1d7eefdbb
|
||||
# 4:9 343:399
|
||||
# 179:200 343:434
|
||||
|
||||
# python eval_tools/temporal_analysis/compare_temporal_stability.py \
|
||||
# --input1 /deeplearning_team/ydong/dongying/projects/yolov5-3d/runs/val_viz_300w/2026-01-07_14-21-06_roi0/tracking.json \
|
||||
# --input2 /deeplearning_team/ydong/dongying/projects/monocular_3d_object_detection/yolov5_mono2d3d_augment_roi_for4face_double_roi_json_rickshaw_g1m3/output/2026-01-07_14-21-06_roi0/tracking.json \
|
||||
# --track-mapping 20:9 488:399 \
|
||||
# --class-id 3 \
|
||||
# --model1-name "yolov5s-30w" \
|
||||
# --model2-name "mono3d" \
|
||||
# --output ./comparison_plots_300w/019b18ef-ca2b-7e97-8ca5-d4b1d7eefdbb_roi0/comparison_report.json \
|
||||
# --output-dir ./comparison_plots_300w/019b18ef-ca2b-7e97-8ca5-d4b1d7eefdbb_roi0
|
||||
|
||||
|
||||
# Set PYTHONPATH to project root for module imports
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
export PYTHONPATH="${PROJECT_ROOT}:${PYTHONPATH}"
|
||||
python eval_tools/temporal_analysis/compare_temporal_stability.py \
|
||||
--input1 /deeplearning_team/ydong/dongying/projects/yolov5-3d/runs/val_viz/2026-01-07_14-21-06_roi0/tracking.json \
|
||||
--input2 /deeplearning_team/ydong/dongying/projects/monocular_3d_object_detection/yolov5_mono2d3d_augment_roi_for4face_double_roi_json_rickshaw_g1m3/output/2026-01-07_14-21-06_roi0/tracking.json \
|
||||
--track-mapping 1:2 8:9 \
|
||||
--class-id 0 \
|
||||
--model1-name "yolov5s-30w" \
|
||||
--model2-name "mono3d" \
|
||||
--output ./comparison_plots_30w/2026-01-07_14-21-06_roi0/comparison_report.json \
|
||||
--output-dir ./comparison_plots_30w/2026-01-07_14-21-06_roi0
|
||||
|
||||
# python eval_tools/temporal_analysis/compare_temporal_stability.py \
|
||||
# --input1 /deeplearning_team/ydong/dongying/projects/yolov5-3d/runs/val_viz/20251210222153_roi0/tracking.json \
|
||||
# --input2 /deeplearning_team/ydong/dongying/projects/monocular_3d_object_detection/yolov5_mono2d3d_augment_roi_for4face_double_roi_json_rickshaw_g1m3/output/20251210222153_roi0/tracking.json \
|
||||
# --track-mapping 1:12 44:53 104:150 \
|
||||
# --class-id 0 \
|
||||
# --model1-name "yolov5s-30w" \
|
||||
# --model2-name "mono3d" \
|
||||
# --output ./comparison_plots_30w/20251210222153_roi0/comparison_report.json \
|
||||
# --output-dir ./comparison_plots_30w/20251210222153_roi0
|
||||
664
tools/temporal_analysis/compare_temporal_stability.py
Executable file
664
tools/temporal_analysis/compare_temporal_stability.py
Executable file
@@ -0,0 +1,664 @@
|
||||
"""
|
||||
Compare temporal stability of 3D predictions between two models.
|
||||
|
||||
Usage:
|
||||
python compare_temporal_stability.py \
|
||||
--input1 runs/model1/tracking.json \
|
||||
--input2 runs/model2/tracking.json \
|
||||
--track-mapping 1:5 2:10 3:15 \
|
||||
--class-id 0 \
|
||||
--output comparison_report.json
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
from collections import defaultdict
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib
|
||||
matplotlib.use('Agg')
|
||||
|
||||
|
||||
def compute_angle_diff(angle1, angle2):
|
||||
"""Compute the smallest difference between two angles in radians.
|
||||
|
||||
Args:
|
||||
angle1, angle2: Angles in radians
|
||||
|
||||
Returns:
|
||||
Angle difference in range [-pi, pi]
|
||||
"""
|
||||
diff = angle1 - angle2
|
||||
while diff > np.pi:
|
||||
diff -= 2 * np.pi
|
||||
while diff < -np.pi:
|
||||
diff += 2 * np.pi
|
||||
return diff
|
||||
|
||||
|
||||
def extract_trajectories(tracking_data):
|
||||
"""Extract trajectories for each tracked object, organized by class.
|
||||
|
||||
Args:
|
||||
tracking_data: List of frame data from tracking.json
|
||||
|
||||
Returns:
|
||||
Dictionary mapping class_id to dict of {track_id: list of (frame_idx, detection) tuples}
|
||||
"""
|
||||
trajectories_by_class = defaultdict(lambda: defaultdict(list))
|
||||
|
||||
for frame_idx, frame_data in enumerate(tracking_data):
|
||||
for det in frame_data.get('detections', []):
|
||||
track_id = det.get('track_id')
|
||||
class_id = det.get('class_id')
|
||||
if track_id is not None and class_id is not None and 'object_3d' in det:
|
||||
trajectories_by_class[class_id][track_id].append((frame_idx, det))
|
||||
|
||||
return trajectories_by_class
|
||||
|
||||
|
||||
def extract_3d_data(trajectory):
|
||||
"""Extract 3D data from trajectory.
|
||||
|
||||
Args:
|
||||
trajectory: List of (frame_idx, detection) tuples
|
||||
|
||||
Returns:
|
||||
Dictionary with extracted data arrays
|
||||
"""
|
||||
# Use dictionary to handle potential duplicate frames (take last occurrence)
|
||||
frame_data = {}
|
||||
|
||||
for frame_idx, det in trajectory:
|
||||
object_3d = det['object_3d']
|
||||
|
||||
# Support both list and dict formats
|
||||
if isinstance(object_3d, dict):
|
||||
x, y, z = object_3d['location']
|
||||
l, h, w = object_3d['dimensions']
|
||||
rot_y = object_3d['rotation_y']
|
||||
else:
|
||||
x, y, z = object_3d[0], object_3d[1], object_3d[2]
|
||||
l, h, w = object_3d[3], object_3d[4], object_3d[5]
|
||||
rot_y = object_3d[6]
|
||||
|
||||
frame_data[frame_idx] = {
|
||||
'position': [x, y, z],
|
||||
'dimension': [l, h, w],
|
||||
'rotation': rot_y
|
||||
}
|
||||
|
||||
# Sort by frame index and extract arrays
|
||||
sorted_frames = sorted(frame_data.keys())
|
||||
frames = []
|
||||
positions = []
|
||||
dimensions = []
|
||||
rotations = []
|
||||
|
||||
for frame_idx in sorted_frames:
|
||||
data = frame_data[frame_idx]
|
||||
frames.append(frame_idx)
|
||||
positions.append(data['position'])
|
||||
dimensions.append(data['dimension'])
|
||||
rotations.append(data['rotation'])
|
||||
|
||||
return {
|
||||
'frames': np.array(frames),
|
||||
'positions': np.array(positions),
|
||||
'dimensions': np.array(dimensions),
|
||||
'rotations': np.array(rotations),
|
||||
}
|
||||
|
||||
|
||||
def compute_trajectory_metrics(data1, data2):
|
||||
"""Compute comparison metrics between two trajectories.
|
||||
|
||||
Args:
|
||||
data1, data2: Extracted 3D data dictionaries
|
||||
|
||||
Returns:
|
||||
Dictionary with comparison metrics
|
||||
"""
|
||||
# Ensure arrays have the same length
|
||||
if len(data1['positions']) != len(data2['positions']):
|
||||
raise ValueError(f"Data arrays have different lengths: {len(data1['positions'])} vs {len(data2['positions'])}")
|
||||
|
||||
# Position differences
|
||||
pos_diff_mean = np.mean(np.linalg.norm(data1['positions'] - data2['positions'], axis=1))
|
||||
pos_diff_std = np.std(np.linalg.norm(data1['positions'] - data2['positions'], axis=1))
|
||||
pos_diff_max = np.max(np.linalg.norm(data1['positions'] - data2['positions'], axis=1))
|
||||
|
||||
# Dimension differences
|
||||
dim_diff = np.abs(data1['dimensions'] - data2['dimensions'])
|
||||
dim_diff_mean = np.mean(dim_diff, axis=0)
|
||||
dim_diff_std = np.std(dim_diff, axis=0)
|
||||
dim_diff_max = np.max(dim_diff, axis=0)
|
||||
|
||||
# Rotation differences
|
||||
rot_diffs = []
|
||||
for r1, r2 in zip(data1['rotations'], data2['rotations']):
|
||||
diff = abs(compute_angle_diff(r1, r2))
|
||||
rot_diffs.append(diff)
|
||||
rot_diffs = np.array(rot_diffs)
|
||||
|
||||
rot_diff_mean = np.mean(rot_diffs)
|
||||
rot_diff_std = np.std(rot_diffs)
|
||||
rot_diff_max = np.max(rot_diffs)
|
||||
|
||||
# Position jitter (frame-to-frame variation)
|
||||
pos_jitter1 = np.linalg.norm(np.diff(data1['positions'], axis=0), axis=1)
|
||||
pos_jitter2 = np.linalg.norm(np.diff(data2['positions'], axis=0), axis=1)
|
||||
|
||||
# Dimension jitter
|
||||
dim_jitter1 = np.abs(np.diff(data1['dimensions'], axis=0))
|
||||
dim_jitter2 = np.abs(np.diff(data2['dimensions'], axis=0))
|
||||
|
||||
# Rotation jitter
|
||||
rot_jitter1 = np.abs(np.diff(data1['rotations']))
|
||||
rot_jitter2 = np.abs(np.diff(data2['rotations']))
|
||||
|
||||
metrics = {
|
||||
'position_difference': {
|
||||
'mean': float(pos_diff_mean),
|
||||
'std': float(pos_diff_std),
|
||||
'max': float(pos_diff_max),
|
||||
},
|
||||
'dimension_difference': {
|
||||
'length_mean': float(dim_diff_mean[0]),
|
||||
'height_mean': float(dim_diff_mean[1]),
|
||||
'width_mean': float(dim_diff_mean[2]),
|
||||
'length_std': float(dim_diff_std[0]),
|
||||
'height_std': float(dim_diff_std[1]),
|
||||
'width_std': float(dim_diff_std[2]),
|
||||
'length_max': float(dim_diff_max[0]),
|
||||
'height_max': float(dim_diff_max[1]),
|
||||
'width_max': float(dim_diff_max[2]),
|
||||
},
|
||||
'rotation_difference': {
|
||||
'mean': float(rot_diff_mean),
|
||||
'std': float(rot_diff_std),
|
||||
'max': float(rot_diff_max),
|
||||
},
|
||||
'position_jitter': {
|
||||
'model1_mean': float(np.mean(pos_jitter1)),
|
||||
'model2_mean': float(np.mean(pos_jitter2)),
|
||||
'model1_std': float(np.std(pos_jitter1)),
|
||||
'model2_std': float(np.std(pos_jitter2)),
|
||||
},
|
||||
'dimension_jitter': {
|
||||
'model1_length_mean': float(np.mean(dim_jitter1[:, 0])),
|
||||
'model2_length_mean': float(np.mean(dim_jitter2[:, 0])),
|
||||
'model1_height_mean': float(np.mean(dim_jitter1[:, 1])),
|
||||
'model2_height_mean': float(np.mean(dim_jitter2[:, 1])),
|
||||
'model1_width_mean': float(np.mean(dim_jitter1[:, 2])),
|
||||
'model2_width_mean': float(np.mean(dim_jitter2[:, 2])),
|
||||
},
|
||||
'rotation_jitter': {
|
||||
'model1_mean': float(np.mean(rot_jitter1)),
|
||||
'model2_mean': float(np.mean(rot_jitter2)),
|
||||
'model1_std': float(np.std(rot_jitter1)),
|
||||
'model2_std': float(np.std(rot_jitter2)),
|
||||
},
|
||||
}
|
||||
|
||||
return metrics
|
||||
|
||||
|
||||
def plot_comparison(data1, data2, track_id1, track_id2, model1_name, model2_name, output_dir, class_id):
|
||||
"""Generate comparison plots for two trajectories.
|
||||
|
||||
Args:
|
||||
data1, data2: Extracted 3D data dictionaries
|
||||
track_id1, track_id2: Track IDs from each model
|
||||
model1_name, model2_name: Model names for labels
|
||||
output_dir: Output directory for plots
|
||||
class_id: Object class ID
|
||||
"""
|
||||
output_dir = Path(output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Plot 1: Position (x, y, z) comparison
|
||||
fig, axes = plt.subplots(3, 1, figsize=(14, 12))
|
||||
|
||||
axes[0].plot(data1['frames'], data1['positions'][:, 0], marker='o', markersize=4,
|
||||
label=f'{model1_name} (Track {track_id1})', alpha=0.7, linewidth=2, color='blue')
|
||||
axes[0].plot(data2['frames'], data2['positions'][:, 0], marker='s', markersize=4,
|
||||
label=f'{model2_name} (Track {track_id2})', alpha=0.7, linewidth=2, color='red')
|
||||
axes[0].set_ylabel('X Position (m)', fontsize=11)
|
||||
axes[0].set_title(f'Class {class_id}: Position Comparison - {model1_name} vs {model2_name}', fontsize=13, fontweight='bold')
|
||||
axes[0].legend(fontsize=10)
|
||||
axes[0].grid(True, alpha=0.3)
|
||||
|
||||
axes[1].plot(data1['frames'], data1['positions'][:, 1], marker='o', markersize=4,
|
||||
label=f'{model1_name} (Track {track_id1})', alpha=0.7, linewidth=2, color='blue')
|
||||
axes[1].plot(data2['frames'], data2['positions'][:, 1], marker='s', markersize=4,
|
||||
label=f'{model2_name} (Track {track_id2})', alpha=0.7, linewidth=2, color='red')
|
||||
axes[1].set_ylabel('Y Position (m)', fontsize=11)
|
||||
axes[1].legend(fontsize=10)
|
||||
axes[1].grid(True, alpha=0.3)
|
||||
|
||||
axes[2].plot(data1['frames'], data1['positions'][:, 2], marker='o', markersize=4,
|
||||
label=f'{model1_name} (Track {track_id1})', alpha=0.7, linewidth=2, color='blue')
|
||||
axes[2].plot(data2['frames'], data2['positions'][:, 2], marker='s', markersize=4,
|
||||
label=f'{model2_name} (Track {track_id2})', alpha=0.7, linewidth=2, color='red')
|
||||
axes[2].set_ylabel('Z Position (m)', fontsize=11)
|
||||
axes[2].set_xlabel('Frame Index', fontsize=11)
|
||||
axes[2].legend(fontsize=10)
|
||||
axes[2].grid(True, alpha=0.3)
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig(output_dir / f'class_{class_id}_track_{track_id1}_{track_id2}_position_comparison.png', dpi=150, bbox_inches='tight')
|
||||
plt.close()
|
||||
|
||||
# Plot 2: Position difference over time
|
||||
fig, ax = plt.subplots(figsize=(14, 5))
|
||||
pos_diff = np.linalg.norm(data1['positions'] - data2['positions'], axis=1)
|
||||
ax.plot(data1['frames'], pos_diff, marker='o', markersize=4, linewidth=2, color='purple', alpha=0.7)
|
||||
ax.fill_between(data1['frames'], 0, pos_diff, alpha=0.3, color='purple')
|
||||
ax.set_xlabel('Frame Index', fontsize=11)
|
||||
ax.set_ylabel('Position Difference (m)', fontsize=11)
|
||||
ax.set_title(f'Class {class_id}: Position Difference - {model1_name} vs {model2_name}', fontsize=13, fontweight='bold')
|
||||
ax.grid(True, alpha=0.3)
|
||||
ax.axhline(y=np.mean(pos_diff), color='red', linestyle='--', label=f'Mean: {np.mean(pos_diff):.3f}m')
|
||||
ax.legend(fontsize=10)
|
||||
plt.tight_layout()
|
||||
plt.savefig(output_dir / f'class_{class_id}_track_{track_id1}_{track_id2}_position_difference.png', dpi=150, bbox_inches='tight')
|
||||
plt.close()
|
||||
|
||||
# Plot 3: Dimensions (l, h, w) comparison
|
||||
fig, axes = plt.subplots(3, 1, figsize=(14, 12))
|
||||
dim_labels = ['Length', 'Height', 'Width']
|
||||
|
||||
for i, label in enumerate(dim_labels):
|
||||
axes[i].plot(data1['frames'], data1['dimensions'][:, i], marker='o', markersize=4,
|
||||
label=f'{model1_name} (Track {track_id1})', alpha=0.7, linewidth=2, color='blue')
|
||||
axes[i].plot(data2['frames'], data2['dimensions'][:, i], marker='s', markersize=4,
|
||||
label=f'{model2_name} (Track {track_id2})', alpha=0.7, linewidth=2, color='red')
|
||||
axes[i].set_ylabel(f'{label} (m)', fontsize=11)
|
||||
if i == 0:
|
||||
axes[i].set_title(f'Class {class_id}: Dimension Comparison - {model1_name} vs {model2_name}', fontsize=13, fontweight='bold')
|
||||
axes[i].legend(fontsize=10)
|
||||
axes[i].grid(True, alpha=0.3)
|
||||
|
||||
axes[2].set_xlabel('Frame Index', fontsize=11)
|
||||
plt.tight_layout()
|
||||
plt.savefig(output_dir / f'class_{class_id}_track_{track_id1}_{track_id2}_dimension_comparison.png', dpi=150, bbox_inches='tight')
|
||||
plt.close()
|
||||
|
||||
# Plot 4: Dimension difference over time
|
||||
fig, axes = plt.subplots(3, 1, figsize=(14, 10))
|
||||
dim_diff = np.abs(data1['dimensions'] - data2['dimensions'])
|
||||
|
||||
for i, label in enumerate(dim_labels):
|
||||
axes[i].plot(data1['frames'], dim_diff[:, i], marker='o', markersize=4, linewidth=2, color='purple', alpha=0.7)
|
||||
axes[i].fill_between(data1['frames'], 0, dim_diff[:, i], alpha=0.3, color='purple')
|
||||
axes[i].set_ylabel(f'{label} Diff (m)', fontsize=11)
|
||||
if i == 0:
|
||||
axes[i].set_title(f'Class {class_id}: Dimension Difference - {model1_name} vs {model2_name}', fontsize=13, fontweight='bold')
|
||||
axes[i].grid(True, alpha=0.3)
|
||||
axes[i].axhline(y=np.mean(dim_diff[:, i]), color='red', linestyle='--',
|
||||
label=f'Mean: {np.mean(dim_diff[:, i]):.4f}m')
|
||||
axes[i].legend(fontsize=10)
|
||||
|
||||
axes[2].set_xlabel('Frame Index', fontsize=11)
|
||||
plt.tight_layout()
|
||||
plt.savefig(output_dir / f'class_{class_id}_track_{track_id1}_{track_id2}_dimension_difference.png', dpi=150, bbox_inches='tight')
|
||||
plt.close()
|
||||
|
||||
# Plot 5: Rotation comparison
|
||||
fig, ax = plt.subplots(figsize=(14, 5))
|
||||
ax.plot(data1['frames'], data1['rotations'], marker='o', markersize=4,
|
||||
label=f'{model1_name} (Track {track_id1})', alpha=0.7, linewidth=2, color='blue')
|
||||
ax.plot(data2['frames'], data2['rotations'], marker='s', markersize=4,
|
||||
label=f'{model2_name} (Track {track_id2})', alpha=0.7, linewidth=2, color='red')
|
||||
ax.set_ylabel('Rotation Y (rad)', fontsize=11)
|
||||
ax.set_xlabel('Frame Index', fontsize=11)
|
||||
ax.set_title(f'Class {class_id}: Rotation Comparison - {model1_name} vs {model2_name}', fontsize=13, fontweight='bold')
|
||||
ax.legend(fontsize=10)
|
||||
ax.grid(True, alpha=0.3)
|
||||
ax.axhline(y=0, color='k', linestyle='--', alpha=0.3)
|
||||
ax.axhline(y=np.pi, color='k', linestyle='--', alpha=0.3)
|
||||
ax.axhline(y=-np.pi, color='k', linestyle='--', alpha=0.3)
|
||||
plt.tight_layout()
|
||||
plt.savefig(output_dir / f'class_{class_id}_track_{track_id1}_{track_id2}_rotation_comparison.png', dpi=150, bbox_inches='tight')
|
||||
plt.close()
|
||||
|
||||
# Plot 6: Rotation difference over time
|
||||
fig, ax = plt.subplots(figsize=(14, 5))
|
||||
rot_diffs = [abs(compute_angle_diff(r1, r2)) for r1, r2 in zip(data1['rotations'], data2['rotations'])]
|
||||
ax.plot(data1['frames'], rot_diffs, marker='o', markersize=4, linewidth=2, color='purple', alpha=0.7)
|
||||
ax.fill_between(data1['frames'], 0, rot_diffs, alpha=0.3, color='purple')
|
||||
ax.set_xlabel('Frame Index', fontsize=11)
|
||||
ax.set_ylabel('Rotation Difference (rad)', fontsize=11)
|
||||
ax.set_title(f'Class {class_id}: Rotation Difference - {model1_name} vs {model2_name}', fontsize=13, fontweight='bold')
|
||||
ax.grid(True, alpha=0.3)
|
||||
ax.axhline(y=np.mean(rot_diffs), color='red', linestyle='--', label=f'Mean: {np.mean(rot_diffs):.4f} rad')
|
||||
ax.legend(fontsize=10)
|
||||
plt.tight_layout()
|
||||
plt.savefig(output_dir / f'class_{class_id}_track_{track_id1}_{track_id2}_rotation_difference.png', dpi=150, bbox_inches='tight')
|
||||
plt.close()
|
||||
|
||||
# Plot 7: 3D trajectory comparison (bird's eye view)
|
||||
fig, ax = plt.subplots(figsize=(12, 12))
|
||||
|
||||
# Plot trajectories
|
||||
ax.plot(data1['positions'][:, 0], data1['positions'][:, 2], marker='o', markersize=5,
|
||||
label=f'{model1_name} (Track {track_id1})', alpha=0.7, linewidth=2.5, color='blue')
|
||||
ax.plot(data2['positions'][:, 0], data2['positions'][:, 2], marker='s', markersize=5,
|
||||
label=f'{model2_name} (Track {track_id2})', alpha=0.7, linewidth=2.5, color='red')
|
||||
|
||||
# Mark start points
|
||||
ax.scatter(data1['positions'][0, 0], data1['positions'][0, 2], s=150, marker='o',
|
||||
edgecolors='darkblue', facecolors='blue', linewidths=3, zorder=5, label=f'{model1_name} Start')
|
||||
ax.scatter(data2['positions'][0, 0], data2['positions'][0, 2], s=150, marker='s',
|
||||
edgecolors='darkred', facecolors='red', linewidths=3, zorder=5, label=f'{model2_name} Start')
|
||||
|
||||
# Mark end points
|
||||
ax.scatter(data1['positions'][-1, 0], data1['positions'][-1, 2], s=150, marker='^',
|
||||
edgecolors='darkblue', facecolors='cyan', linewidths=3, zorder=5, label=f'{model1_name} End')
|
||||
ax.scatter(data2['positions'][-1, 0], data2['positions'][-1, 2], s=150, marker='^',
|
||||
edgecolors='darkred', facecolors='orange', linewidths=3, zorder=5, label=f'{model2_name} End')
|
||||
|
||||
# Draw lines connecting corresponding points
|
||||
for i in range(len(data1['frames'])):
|
||||
ax.plot([data1['positions'][i, 0], data2['positions'][i, 0]],
|
||||
[data1['positions'][i, 2], data2['positions'][i, 2]],
|
||||
color='gray', alpha=0.3, linewidth=1, linestyle='--', zorder=1)
|
||||
|
||||
ax.set_xlabel('X Position (m)', fontsize=11)
|
||||
ax.set_ylabel('Z Position (m)', fontsize=11)
|
||||
ax.set_title(f'Class {class_id}: 3D Trajectory Comparison (Bird\'s Eye View)\n{model1_name} vs {model2_name}',
|
||||
fontsize=13, fontweight='bold')
|
||||
ax.legend(fontsize=9, loc='best')
|
||||
ax.grid(True, alpha=0.3)
|
||||
ax.axis('equal')
|
||||
plt.tight_layout()
|
||||
plt.savefig(output_dir / f'class_{class_id}_track_{track_id1}_{track_id2}_trajectory_birds_eye.png', dpi=150, bbox_inches='tight')
|
||||
plt.close()
|
||||
|
||||
# Plot 8: Jitter comparison (position stability)
|
||||
fig, ax = plt.subplots(figsize=(14, 5))
|
||||
pos_jitter1 = np.linalg.norm(np.diff(data1['positions'], axis=0), axis=1)
|
||||
pos_jitter2 = np.linalg.norm(np.diff(data2['positions'], axis=0), axis=1)
|
||||
frames_jitter = data1['frames'][1:] # Skip first frame since we're using diff
|
||||
|
||||
ax.plot(frames_jitter, pos_jitter1, marker='o', markersize=4,
|
||||
label=f'{model1_name} Position Jitter', alpha=0.7, linewidth=2, color='blue')
|
||||
ax.plot(frames_jitter, pos_jitter2, marker='s', markersize=4,
|
||||
label=f'{model2_name} Position Jitter', alpha=0.7, linewidth=2, color='red')
|
||||
ax.set_xlabel('Frame Index', fontsize=11)
|
||||
ax.set_ylabel('Position Jitter (m/frame)', fontsize=11)
|
||||
ax.set_title(f'Class {class_id}: Position Stability Comparison - {model1_name} vs {model2_name}', fontsize=13, fontweight='bold')
|
||||
ax.legend(fontsize=10)
|
||||
ax.grid(True, alpha=0.3)
|
||||
ax.axhline(y=np.mean(pos_jitter1), color='blue', linestyle='--', alpha=0.5, label=f'{model1_name} Mean: {np.mean(pos_jitter1):.4f}')
|
||||
ax.axhline(y=np.mean(pos_jitter2), color='red', linestyle='--', alpha=0.5, label=f'{model2_name} Mean: {np.mean(pos_jitter2):.4f}')
|
||||
plt.tight_layout()
|
||||
plt.savefig(output_dir / f'class_{class_id}_track_{track_id1}_{track_id2}_jitter_comparison.png', dpi=150, bbox_inches='tight')
|
||||
plt.close()
|
||||
|
||||
print(f"Comparison plots saved to {output_dir}")
|
||||
|
||||
|
||||
def parse_track_mapping(mapping_strings):
|
||||
"""Parse track mapping from command line arguments.
|
||||
|
||||
Args:
|
||||
mapping_strings: List of strings in format "id1:id2"
|
||||
|
||||
Returns:
|
||||
List of tuples (id1, id2)
|
||||
"""
|
||||
mappings = []
|
||||
for s in mapping_strings:
|
||||
try:
|
||||
id1, id2 = s.split(':')
|
||||
mappings.append((int(id1), int(id2)))
|
||||
except ValueError:
|
||||
print(f"Warning: Invalid mapping format '{s}', expected 'id1:id2'")
|
||||
continue
|
||||
return mappings
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Compare temporal stability between two models')
|
||||
parser.add_argument('--input1', type=str, required=True, help='First tracking.json file path (model 1)')
|
||||
parser.add_argument('--input2', type=str, required=True, help='Second tracking.json file path (model 2)')
|
||||
parser.add_argument('--track-mapping', type=str, nargs='+', required=True,
|
||||
help='Track ID mappings in format "id1:id2" (e.g., --track-mapping 1:5 2:10)')
|
||||
parser.add_argument('--class-id', type=int, default=None,
|
||||
help='Filter by class ID (optional)')
|
||||
parser.add_argument('--model1-name', type=str, default='Model 1',
|
||||
help='Name for first model (for plot labels)')
|
||||
parser.add_argument('--model2-name', type=str, default='Model 2',
|
||||
help='Name for second model (for plot labels)')
|
||||
parser.add_argument('--output', type=str, default='comparison_report.json',
|
||||
help='Output comparison report JSON file path')
|
||||
parser.add_argument('--output-dir', type=str, default=None,
|
||||
help='Output directory for plots (default: same as output JSON)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Read input tracking data
|
||||
input1_path = Path(args.input1)
|
||||
input2_path = Path(args.input2)
|
||||
|
||||
if not input1_path.exists():
|
||||
print(f"Error: Input file 1 not found: {input1_path}")
|
||||
return
|
||||
if not input2_path.exists():
|
||||
print(f"Error: Input file 2 not found: {input2_path}")
|
||||
return
|
||||
|
||||
print(f"Loading tracking data from:")
|
||||
print(f" Model 1: {input1_path}")
|
||||
print(f" Model 2: {input2_path}")
|
||||
|
||||
with open(input1_path, 'r', encoding='utf-8') as f:
|
||||
tracking_data1 = json.load(f)
|
||||
with open(input2_path, 'r', encoding='utf-8') as f:
|
||||
tracking_data2 = json.load(f)
|
||||
|
||||
print(f"Loaded {len(tracking_data1)} frames from model 1")
|
||||
print(f"Loaded {len(tracking_data2)} frames from model 2")
|
||||
|
||||
print("Sorting frames by image name...")
|
||||
tracking_data1.sort(key=lambda x: x.get('image_name', ''))
|
||||
print("Sorting frames by image name...")
|
||||
tracking_data2.sort(key=lambda x: x.get('image_name', ''))
|
||||
|
||||
# Extract trajectories
|
||||
print("\nExtracting trajectories...")
|
||||
trajectories_by_class1 = extract_trajectories(tracking_data1)
|
||||
trajectories_by_class2 = extract_trajectories(tracking_data2)
|
||||
|
||||
total_tracks1 = sum(len(tracks) for tracks in trajectories_by_class1.values())
|
||||
total_tracks2 = sum(len(tracks) for tracks in trajectories_by_class2.values())
|
||||
print(f"Model 1: {total_tracks1} unique tracks across {len(trajectories_by_class1)} classes")
|
||||
for class_id, tracks in sorted(trajectories_by_class1.items()):
|
||||
print(f" Class {class_id}: {len(tracks)} tracks")
|
||||
print(f"Model 2: {total_tracks2} unique tracks across {len(trajectories_by_class2)} classes")
|
||||
for class_id, tracks in sorted(trajectories_by_class2.items()):
|
||||
print(f" Class {class_id}: {len(tracks)} tracks")
|
||||
|
||||
# Parse track mappings
|
||||
track_mappings = parse_track_mapping(args.track_mapping)
|
||||
print(f"\nTrack mappings: {track_mappings}")
|
||||
|
||||
# Filter by class ID if specified and flatten the trajectories dict
|
||||
if args.class_id is not None:
|
||||
trajectories1 = trajectories_by_class1.get(args.class_id, {})
|
||||
trajectories2 = trajectories_by_class2.get(args.class_id, {})
|
||||
print(f"\nFiltered to class_id={args.class_id}:")
|
||||
print(f" Model 1: {len(trajectories1)} tracks")
|
||||
print(f" Model 2: {len(trajectories2)} tracks")
|
||||
else:
|
||||
# Flatten all classes into single dict if no class filter specified
|
||||
trajectories1 = {}
|
||||
for class_tracks in trajectories_by_class1.values():
|
||||
trajectories1.update(class_tracks)
|
||||
trajectories2 = {}
|
||||
for class_tracks in trajectories_by_class2.values():
|
||||
trajectories2.update(class_tracks)
|
||||
print(f"\nUsing all classes:")
|
||||
print(f" Model 1: {len(trajectories1)} total tracks")
|
||||
print(f" Model 2: {len(trajectories2)} total tracks")
|
||||
|
||||
# Determine output directory
|
||||
output_path = Path(args.output)
|
||||
if args.output_dir:
|
||||
output_dir = Path(args.output_dir)
|
||||
else:
|
||||
output_dir = output_path.parent / 'comparison_plots'
|
||||
|
||||
# Process each track mapping
|
||||
comparison_results = []
|
||||
|
||||
for track_id1, track_id2 in track_mappings:
|
||||
print(f"\n{'='*80}")
|
||||
print(f"Processing track mapping: Model 1 Track {track_id1} <-> Model 2 Track {track_id2}")
|
||||
print('='*80)
|
||||
|
||||
# Check if tracks exist
|
||||
if track_id1 not in trajectories1:
|
||||
print(f"Warning: Track {track_id1} not found in model 1, skipping")
|
||||
continue
|
||||
if track_id2 not in trajectories2:
|
||||
print(f"Warning: Track {track_id2} not found in model 2, skipping")
|
||||
continue
|
||||
|
||||
traj1 = trajectories1[track_id1]
|
||||
traj2 = trajectories2[track_id2]
|
||||
|
||||
# Get class IDs
|
||||
class_id1 = traj1[0][1].get('class_id')
|
||||
class_id2 = traj2[0][1].get('class_id')
|
||||
|
||||
# Filter by class if specified
|
||||
if args.class_id is not None:
|
||||
if class_id1 != args.class_id:
|
||||
print(f"Warning: Track {track_id1} has class_id={class_id1}, expected {args.class_id}, skipping")
|
||||
continue
|
||||
if class_id2 != args.class_id:
|
||||
print(f"Warning: Track {track_id2} has class_id={class_id2}, expected {args.class_id}, skipping")
|
||||
continue
|
||||
|
||||
# Check if classes match
|
||||
if class_id1 != class_id2:
|
||||
print(f"Warning: Class mismatch - Track {track_id1} (class {class_id1}) vs Track {track_id2} (class {class_id2})")
|
||||
print("Continuing with comparison, but results may not be meaningful")
|
||||
|
||||
print(f"Model 1 Track {track_id1}: {len(traj1)} frames, class {class_id1}")
|
||||
print(f"Model 2 Track {track_id2}: {len(traj2)} frames, class {class_id2}")
|
||||
|
||||
# Find common frame range
|
||||
frames1 = set([f for f, _ in traj1])
|
||||
frames2 = set([f for f, _ in traj2])
|
||||
common_frames = sorted(frames1.intersection(frames2))
|
||||
|
||||
if len(common_frames) < 2:
|
||||
print(f"Warning: Only {len(common_frames)} common frames, need at least 2 for comparison, skipping")
|
||||
continue
|
||||
|
||||
print(f"Common frames: {len(common_frames)} ({min(common_frames)} to {max(common_frames)})")
|
||||
|
||||
# Filter trajectories to common frames and remove duplicates
|
||||
# Use dict to keep only one detection per frame (last occurrence)
|
||||
traj1_dict = {f: d for f, d in traj1 if f in common_frames}
|
||||
traj2_dict = {f: d for f, d in traj2 if f in common_frames}
|
||||
|
||||
# Ensure both have exactly the same frames
|
||||
final_common_frames = sorted(set(traj1_dict.keys()).intersection(set(traj2_dict.keys())))
|
||||
|
||||
if len(final_common_frames) < 2:
|
||||
print(f"Warning: After deduplication, only {len(final_common_frames)} common frames, skipping")
|
||||
continue
|
||||
|
||||
# Convert back to sorted list of tuples
|
||||
traj1_filtered = [(f, traj1_dict[f]) for f in final_common_frames]
|
||||
traj2_filtered = [(f, traj2_dict[f]) for f in final_common_frames]
|
||||
|
||||
print(f"After alignment: {len(traj1_filtered)} frames for comparison")
|
||||
|
||||
# Extract 3D data
|
||||
data1 = extract_3d_data(traj1_filtered)
|
||||
data2 = extract_3d_data(traj2_filtered)
|
||||
|
||||
# Compute metrics
|
||||
metrics = compute_trajectory_metrics(data1, data2)
|
||||
|
||||
# Generate plots
|
||||
plot_comparison(data1, data2, track_id1, track_id2,
|
||||
args.model1_name, args.model2_name, output_dir, class_id1)
|
||||
|
||||
# Store results
|
||||
comparison_results.append({
|
||||
'track_id_model1': track_id1,
|
||||
'track_id_model2': track_id2,
|
||||
'class_id_model1': class_id1,
|
||||
'class_id_model2': class_id2,
|
||||
'num_common_frames': len(common_frames),
|
||||
'frame_range': [min(common_frames), max(common_frames)],
|
||||
'metrics': metrics,
|
||||
})
|
||||
|
||||
# Print summary for this pair
|
||||
print(f"\nComparison Metrics:")
|
||||
print(f" Position Difference: {metrics['position_difference']['mean']:.4f} ± {metrics['position_difference']['std']:.4f} m (max: {metrics['position_difference']['max']:.4f})")
|
||||
print(f" Dimension Difference (L/H/W): {metrics['dimension_difference']['length_mean']:.4f} / {metrics['dimension_difference']['height_mean']:.4f} / {metrics['dimension_difference']['width_mean']:.4f} m")
|
||||
print(f" Rotation Difference: {metrics['rotation_difference']['mean']:.4f} ± {metrics['rotation_difference']['std']:.4f} rad (max: {metrics['rotation_difference']['max']:.4f})")
|
||||
print(f" Position Jitter (Model1/Model2): {metrics['position_jitter']['model1_mean']:.4f} / {metrics['position_jitter']['model2_mean']:.4f} m/frame")
|
||||
print(f" Rotation Jitter (Model1/Model2): {metrics['rotation_jitter']['model1_mean']:.4f} / {metrics['rotation_jitter']['model2_mean']:.4f} rad/frame")
|
||||
|
||||
# Save comparison report
|
||||
report = {
|
||||
'model1': {
|
||||
'input_file': str(input1_path),
|
||||
'name': args.model1_name,
|
||||
'total_frames': len(tracking_data1),
|
||||
'total_tracks': len(trajectories1),
|
||||
},
|
||||
'model2': {
|
||||
'input_file': str(input2_path),
|
||||
'name': args.model2_name,
|
||||
'total_frames': len(tracking_data2),
|
||||
'total_tracks': len(trajectories2),
|
||||
},
|
||||
'comparison_results': comparison_results,
|
||||
'summary': {
|
||||
'num_comparisons': len(comparison_results),
|
||||
'filter_class_id': args.class_id,
|
||||
}
|
||||
}
|
||||
|
||||
print(f"\n{'='*80}")
|
||||
print("Saving comparison report...")
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(report, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"Report saved to: {output_path}")
|
||||
print(f"Plots saved to: {output_dir}")
|
||||
|
||||
# Print overall summary
|
||||
print(f"\n{'='*80}")
|
||||
print("COMPARISON SUMMARY")
|
||||
print('='*80)
|
||||
print(f"Model 1: {args.model1_name}")
|
||||
print(f"Model 2: {args.model2_name}")
|
||||
print(f"Successful comparisons: {len(comparison_results)}")
|
||||
|
||||
if comparison_results:
|
||||
# Compute aggregate statistics
|
||||
avg_pos_diff = np.mean([r['metrics']['position_difference']['mean'] for r in comparison_results])
|
||||
avg_rot_diff = np.mean([r['metrics']['rotation_difference']['mean'] for r in comparison_results])
|
||||
|
||||
print(f"\nAggregate Statistics:")
|
||||
print(f" Average position difference: {avg_pos_diff:.4f} m")
|
||||
print(f" Average rotation difference: {avg_rot_diff:.4f} rad ({np.rad2deg(avg_rot_diff):.2f}°)")
|
||||
|
||||
print("\nComparison completed!")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
19
tools/temporal_analysis/evaluate_temporal.sh
Executable file
19
tools/temporal_analysis/evaluate_temporal.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Set PYTHONPATH to project root for module imports
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
export PYTHONPATH="${PROJECT_ROOT}:${PYTHONPATH}"
|
||||
|
||||
INPUT=/data1/dongying/Mono3d/G1M3/cases_feishu_results/torchscript_python_300w_newdata-cncap/6676060198_20260109113210_2026-01-09_11-33-53_voice-顿挫/merge_tracking.json
|
||||
INPUT_DIR=$(dirname "$INPUT")
|
||||
|
||||
python tools/temporal_analysis/evaluate_temporal_stability.py \
|
||||
--input "$INPUT" \
|
||||
--class-id 0 \
|
||||
--track-ids 3 \
|
||||
--output "$INPUT_DIR/stability_car_123.json" \
|
||||
--plots
|
||||
|
||||
# id: 2, 41, 102
|
||||
# id: 2, 33, 131
|
||||
1356
tools/temporal_analysis/evaluate_temporal_stability.py
Executable file
1356
tools/temporal_analysis/evaluate_temporal_stability.py
Executable file
File diff suppressed because it is too large
Load Diff
498
tools/temporal_analysis/heading_debug_temporal_analysis.md
Executable file
498
tools/temporal_analysis/heading_debug_temporal_analysis.md
Executable file
@@ -0,0 +1,498 @@
|
||||
# Heading Debug 时序分析方案
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
当前 `merge_json`、`roi0`、`roi1` 结果文件中已经保存了 `heading_debug` 字段,包含:
|
||||
|
||||
- `yaw_bin`
|
||||
- `yaw_delta`
|
||||
- `rot_y_decoded`
|
||||
- `yaw_probs`
|
||||
|
||||
这些字段可以帮助区分 heading 不稳定到底来自:
|
||||
|
||||
1. `yaw_bin` 分类抖动
|
||||
2. `yaw_delta` 回归不稳
|
||||
3. `+pi / -pi` 附近的角度回绕假跳变
|
||||
4. ROI0 / ROI1 分支选择不一致
|
||||
5. 遮挡、截断、远距离带来的朝向歧义
|
||||
|
||||
本文档给出一套基于 `heading_debug` 的时序分析方案,目标是回答以下问题:
|
||||
|
||||
- heading 不稳定主要发生在哪些类别、距离段、ROI 分支中
|
||||
- 不稳定的主因是 bin 选错,还是 delta 抖动
|
||||
- merged 结果的不稳定是来自模型本身,还是来自 ROI merge / NMS 选择
|
||||
- 是否存在 `+pi / -pi` 表达带来的假性跳变
|
||||
|
||||
## 2. 推荐输入数据
|
||||
|
||||
建议优先使用以下结果作为分析输入:
|
||||
|
||||
- `merge_json/*.json`
|
||||
- `roi0/*.json`
|
||||
- `roi1/*.json`
|
||||
- `track_objects.py` 输出的 tracking 结果,如果已有 `track_id`
|
||||
- `frame_timestamps_interp.csv`,如果需要按真实时间间隔分析
|
||||
|
||||
如果已经走过 `tools/temporal_analysis/track_objects.py`,则推荐直接在 tracking 结果基础上分析,因为其中通常已经补充了:
|
||||
|
||||
- `track_id`
|
||||
- `frameId`
|
||||
- `timestamp`
|
||||
- `sub_cls`
|
||||
|
||||
如果暂时没有 `track_id`,也可以先做 frame-level 分析,或者用相邻帧 IoU / 3D center 最近邻做临时关联。
|
||||
|
||||
## 3. `heading_debug` 字段解释
|
||||
|
||||
### 3.1 原始字段
|
||||
|
||||
每条 detection 中新增:
|
||||
|
||||
```json
|
||||
"heading_debug": {
|
||||
"yaw_bin": 2,
|
||||
"yaw_delta": 0.137,
|
||||
"rot_y_decoded": -1.434,
|
||||
"yaw_probs": [0.01, 0.08, 0.91, 0.03]
|
||||
}
|
||||
```
|
||||
|
||||
含义如下:
|
||||
|
||||
- `yaw_bin`: 当前选中的 orientation bin,取值为 `0/1/2/3`
|
||||
- `yaw_delta`: 当前选中 bin 内的 residual
|
||||
- `rot_y_decoded`: 按 `yaw_bin + yaw_delta` 解码后的相机系朝向角
|
||||
- `yaw_probs`: 4 个 bin 的 sigmoid 分数
|
||||
|
||||
### 3.2 注意事项
|
||||
|
||||
- `rot_y_decoded` 与 `xyzlhwyaw[6]` 都是相机系 yaw,单位是弧度,可以直接比较。
|
||||
- `xyzlhwyaw_ego[6]` 在当前脚本中是角度制 `deg`,不要和前两者直接混用。
|
||||
- `yaw_probs` 是对 4 个 bin 分别做 sigmoid 得到的分数,不是 softmax 概率。
|
||||
- 不应使用“4 个值求和为 1”的理解。
|
||||
- 更应关注最大分数、次大分数以及它们的差值。
|
||||
|
||||
## 4. 数据整理建议
|
||||
|
||||
建议先把 JSON 扁平化成一张分析表,每行对应一条 detection。至少保留以下字段:
|
||||
|
||||
- `case_name`
|
||||
- `image_name`
|
||||
- `frameId`
|
||||
- `timestamp`
|
||||
- `track_id`,如果有
|
||||
- `type` / `class_id` / `sub_cls`
|
||||
- `score`
|
||||
- `roi_id`
|
||||
- `box2d`
|
||||
- `xyzlhwyaw`
|
||||
- `face_cls`
|
||||
- `cut_cls`
|
||||
- `heading_debug.yaw_bin`
|
||||
- `heading_debug.yaw_delta`
|
||||
- `heading_debug.rot_y_decoded`
|
||||
- `heading_debug.yaw_probs`
|
||||
|
||||
建议继续派生以下分析字段:
|
||||
|
||||
- `x1, y1, x2, y2`
|
||||
- `box_w = x2 - x1`
|
||||
- `box_h = y2 - y1`
|
||||
- `box_area = box_w * box_h`
|
||||
- `depth_z = xyzlhwyaw[2]`
|
||||
- `yaw_final = xyzlhwyaw[6]`
|
||||
- `p1 = max(yaw_probs)`
|
||||
- `p2 = second_max(yaw_probs)`
|
||||
- `margin = p1 - p2`
|
||||
- `num_pos_bins = count(yaw_probs > 0.5)`
|
||||
|
||||
### 4.1 推荐扁平表输出
|
||||
|
||||
建议至少落两份中间结果:
|
||||
|
||||
- `heading_debug_flat.csv`
|
||||
- `heading_debug_track_summary.csv`
|
||||
|
||||
其中:
|
||||
|
||||
- `heading_debug_flat.csv` 用于逐 detection 排查
|
||||
- `heading_debug_track_summary.csv` 用于 track 级汇总和排序
|
||||
|
||||
## 5. 第一轮:做全局统计总览
|
||||
|
||||
在看单条 case 前,先做整体统计,回答“不稳定集中出现在哪里”。
|
||||
|
||||
建议统计以下分布:
|
||||
|
||||
- 各类别的 `yaw_bin` 分布
|
||||
- 各类别的 `margin` 分布
|
||||
- 各类别在不同 `depth_z` 范围下的 `margin` 分布
|
||||
- 各类别在不同 `box_area` 范围下的稳定性指标
|
||||
- `roi_id = 0/1` 的稳定性差异
|
||||
- `face_cls` / `cut_cls` 不同取值下的 heading 波动大小
|
||||
|
||||
重点观察:
|
||||
|
||||
- 不稳定是否集中在远距离、小框、低分样本
|
||||
- 不稳定是否明显偏向某个 ROI 分支
|
||||
- 不稳定是否集中在某个类别或某种可见面
|
||||
|
||||
## 6. 第二轮:做时序稳定性分析
|
||||
|
||||
如果具备 `track_id`,建议以 track 为基本单位分析。
|
||||
|
||||
### 6.1 必须做角度 wrap
|
||||
|
||||
直接做 `yaw_t - yaw_t-1` 会把 `+179° -> -179°` 误判成大跳变,因此角度差必须按环形处理:
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
|
||||
def wrapped_angle_diff(a, b):
|
||||
return np.arctan2(np.sin(a - b), np.cos(a - b))
|
||||
```
|
||||
|
||||
其中:
|
||||
|
||||
- `a`、`b` 为弧度制 yaw
|
||||
- `wrapped_angle_diff(a, b)` 输出范围约为 `[-pi, pi]`
|
||||
|
||||
### 6.2 推荐 track 级指标
|
||||
|
||||
对每条 track 统计:
|
||||
|
||||
- `track_len`
|
||||
- `median(|Δyaw|)`
|
||||
- `p95(|Δyaw|)`
|
||||
- `max(|Δyaw|)`
|
||||
- `yaw_bin_switch_count`
|
||||
- `yaw_bin_switch_rate`
|
||||
- `median(|Δyaw_delta|)`
|
||||
- `max(|Δyaw_delta|)`
|
||||
- `low_margin_ratio`,例如 `margin < 0.1` 的占比
|
||||
- `roi_switch_count`
|
||||
- `face_cls_switch_count`
|
||||
- `cut_cls_switch_count`
|
||||
|
||||
其中:
|
||||
|
||||
- `Δyaw` 使用 wrap 后角度差
|
||||
- `Δyaw_delta` 只建议在相邻帧 `yaw_bin` 相同的条件下统计
|
||||
|
||||
### 6.3 推荐时序样本筛选方式
|
||||
|
||||
可以先筛出最不稳定的 track:
|
||||
|
||||
- `max(|Δyaw|)` 最大的 Top-K
|
||||
- `p95(|Δyaw|)` 最大的 Top-K
|
||||
- `yaw_bin_switch_rate` 最高的 Top-K
|
||||
|
||||
优先人工查看这些 track,对定位问题最有效。
|
||||
|
||||
## 7. 根因归类规则
|
||||
|
||||
可以根据 `heading_debug` 的表现把不稳定样本归成以下几类。
|
||||
|
||||
### 7.1 `yaw_bin` 抖动型
|
||||
|
||||
特征:
|
||||
|
||||
- 相邻帧 `yaw_bin` 频繁切换
|
||||
- 同时 `margin` 较小,例如 `< 0.1`
|
||||
- 或 `num_pos_bins >= 2`
|
||||
|
||||
说明:
|
||||
|
||||
- 问题更偏向 bin 分类头不稳定
|
||||
- 常见于朝向模糊、遮挡、远距离、小目标
|
||||
|
||||
### 7.2 `yaw_delta` 抖动型
|
||||
|
||||
特征:
|
||||
|
||||
- 相邻帧 `yaw_bin` 基本不变
|
||||
- 但 `|Δyaw_delta|` 很大
|
||||
- `margin` 还比较稳定
|
||||
|
||||
说明:
|
||||
|
||||
- bin 选对了,但 bin 内 residual 回归不稳定
|
||||
- 更像回归头噪声,而不是分类歧义
|
||||
|
||||
### 7.3 `+pi / -pi` 回绕假跳变型
|
||||
|
||||
特征:
|
||||
|
||||
- 样本集中在 `yaw_bin = 3` 或接近 `±pi`
|
||||
- 原始 yaw 数值正负跳变明显
|
||||
- 但 wrap 后 `|Δyaw|` 实际并不大
|
||||
|
||||
说明:
|
||||
|
||||
- 这类更多是角度表示问题,不一定是真正的 heading 不稳
|
||||
|
||||
### 7.4 ROI 敏感型
|
||||
|
||||
特征:
|
||||
|
||||
- ROI0 和 ROI1 分支对同一目标给出的 `heading_debug` 差异较大
|
||||
- merged 结果的不稳定伴随 `roi_id` 切换
|
||||
|
||||
说明:
|
||||
|
||||
- 问题可能来自 crop 视角差异、ROI 边界、分支选择或 cross-ROI merge
|
||||
|
||||
### 7.5 可见性 / 朝向歧义型
|
||||
|
||||
特征:
|
||||
|
||||
- heading 跳变时伴随 `score` 低、框小、距离远
|
||||
- 同时 `face_cls` 或 `cut_cls` 也在变化
|
||||
|
||||
说明:
|
||||
|
||||
- 更像输入信息不足导致的朝向歧义,而非单纯模型数值抖动
|
||||
|
||||
### 7.6 180 度翻转型
|
||||
|
||||
特征:
|
||||
|
||||
- 相邻帧 `|Δyaw|` 接近 `pi`
|
||||
- 同时 `face_cls` 可能发生 `front <-> tail` 切换
|
||||
|
||||
说明:
|
||||
|
||||
- 需要重点怀疑前后向语义混淆
|
||||
- 通常不只是小噪声,而是 orientation 语义层面的错误
|
||||
|
||||
## 8. ROI0 / ROI1 / merged 的分层分析建议
|
||||
|
||||
强烈建议不要只看 `merge_json`,而是同时对比:
|
||||
|
||||
- `roi0/*.json`
|
||||
- `roi1/*.json`
|
||||
- `merge_json/*.json`
|
||||
|
||||
可以按以下逻辑判断问题来源:
|
||||
|
||||
### 情况 1:ROI0 和 ROI1 都稳定,但 merged 不稳定
|
||||
|
||||
说明:
|
||||
|
||||
- 更可能是 cross-ROI NMS / merge / 分支选择问题
|
||||
|
||||
### 情况 2:ROI0 和 ROI1 本身差异很大
|
||||
|
||||
说明:
|
||||
|
||||
- 更可能是模型对不同 crop 视角敏感
|
||||
- 需要结合 ROI 边界、目标位置、可见面一起看
|
||||
|
||||
### 情况 3:ROI0、ROI1、merged 都不稳定
|
||||
|
||||
说明:
|
||||
|
||||
- 更可能是模型本身 heading 头不稳
|
||||
- 或目标本身确实难判定
|
||||
|
||||
## 9. 如果有 GT,建议把误差拆成“bin 错”还是“delta 错”
|
||||
|
||||
如果测试数据带朝向标注,可以把 GT yaw 也编码成 `gt_bin + gt_delta`,编码规则与后处理保持一致:
|
||||
|
||||
- `[-pi/4, pi/4)` -> `bin 0`, `delta = yaw`
|
||||
- `[pi/4, 3pi/4)` -> `bin 1`, `delta = yaw - pi/2`
|
||||
- `[-3pi/4, -pi/4)` -> `bin 2`, `delta = yaw + pi/2`
|
||||
- 其他区域 -> `bin 3`
|
||||
- `yaw > 0` 时 `delta = yaw - pi`
|
||||
- `yaw <= 0` 时 `delta = yaw + pi`
|
||||
|
||||
然后分别统计:
|
||||
|
||||
- `pred_bin == gt_bin` 的比例
|
||||
- 在 `pred_bin == gt_bin` 条件下,`|pred_delta - gt_delta|`
|
||||
- 最终 `|wrapped(pred_yaw - gt_yaw)|`
|
||||
|
||||
这样可以直接判断:
|
||||
|
||||
- 如果主要问题是 `pred_bin != gt_bin`,则偏向分类头问题
|
||||
- 如果 `pred_bin` 大多正确,但 `delta` 偏差大,则偏向回归头问题
|
||||
|
||||
## 10. 推荐优先产出的图表
|
||||
|
||||
建议优先做以下 4 类图:
|
||||
|
||||
### 10.1 Track yaw 时序图
|
||||
|
||||
横轴:
|
||||
|
||||
- `frameId` 或 `timestamp`
|
||||
|
||||
纵轴:
|
||||
|
||||
- `yaw_final` 或 `rot_y_decoded`
|
||||
|
||||
同时:
|
||||
|
||||
- 用颜色标出 `yaw_bin`
|
||||
- 标记 `roi_id` 切换点
|
||||
- 标记 `margin` 低于阈值的时刻
|
||||
|
||||
用途:
|
||||
|
||||
- 最直观地看 heading 跳变和 bin 切换的关系
|
||||
|
||||
### 10.2 `|Δyaw|` vs `margin` 散点图
|
||||
|
||||
横轴:
|
||||
|
||||
- `margin`
|
||||
|
||||
纵轴:
|
||||
|
||||
- `|Δyaw|`
|
||||
|
||||
用途:
|
||||
|
||||
- 判断大跳变是否集中在低置信 bin 竞争区域
|
||||
|
||||
### 10.3 `yaw_bin(t-1) -> yaw_bin(t)` 转移矩阵
|
||||
|
||||
用途:
|
||||
|
||||
- 看哪些 bin 最容易互相跳转
|
||||
- 尤其关注 `0 <-> 3`、`1 <-> 2`、`front / tail` 相关模式
|
||||
|
||||
### 10.4 分桶稳定性统计图
|
||||
|
||||
按以下维度分桶:
|
||||
|
||||
- `class`
|
||||
- `roi_id`
|
||||
- `depth_z`
|
||||
- `box_area`
|
||||
- `face_cls`
|
||||
|
||||
统计每个桶里的:
|
||||
|
||||
- `median(|Δyaw|)`
|
||||
- `p95(|Δyaw|)`
|
||||
- `yaw_bin_switch_rate`
|
||||
- `low_margin_ratio`
|
||||
|
||||
用途:
|
||||
|
||||
- 快速定位最差场景
|
||||
|
||||
## 11. 推荐执行顺序
|
||||
|
||||
建议按以下顺序推进:
|
||||
|
||||
### 第一步:做全量统计
|
||||
|
||||
先找出不稳定最严重的:
|
||||
|
||||
- 类别
|
||||
- ROI
|
||||
- 距离段
|
||||
- 框尺度区间
|
||||
|
||||
### 第二步:抽 Top-K 最差 track
|
||||
|
||||
建议优先抽:
|
||||
|
||||
- `max(|Δyaw|)` 最大的 Top-K
|
||||
- `yaw_bin_switch_rate` 最高的 Top-K
|
||||
- `low_margin_ratio` 最高的 Top-K
|
||||
|
||||
### 第三步:逐条排查根因
|
||||
|
||||
每条 track 同时看:
|
||||
|
||||
- `yaw_bin`
|
||||
- `yaw_delta`
|
||||
- `yaw_probs`
|
||||
- `margin`
|
||||
- `roi_id`
|
||||
- `face_cls`
|
||||
- `cut_cls`
|
||||
- `score`
|
||||
- `depth_z`
|
||||
|
||||
### 第四步:归因总结
|
||||
|
||||
把问题归类到以下一种或几种:
|
||||
|
||||
- `yaw_bin` 抖动型
|
||||
- `yaw_delta` 抖动型
|
||||
- 角度回绕假跳变型
|
||||
- ROI 敏感型
|
||||
- 可见性歧义型
|
||||
- 180 度翻转型
|
||||
|
||||
## 12. 最终建议输出物
|
||||
|
||||
建议最终沉淀以下分析产物:
|
||||
|
||||
- `heading_debug_flat.csv`
|
||||
- `heading_debug_track_summary.csv`
|
||||
- `heading_debug_bad_tracks.csv`
|
||||
- `fig_track_yaw_series/` 目录
|
||||
- `fig_margin_vs_dyaw.png`
|
||||
- `fig_bin_transition_matrix.png`
|
||||
- `fig_stability_by_depth.png`
|
||||
- `fig_stability_by_roi.png`
|
||||
- `heading_debug_analysis_report.md`
|
||||
|
||||
其中 `heading_debug_analysis_report.md` 建议包含:
|
||||
|
||||
- 哪些类别最不稳定
|
||||
- 哪些场景最不稳定
|
||||
- 主要根因归类占比
|
||||
- 是否更偏向 bin 问题还是 delta 问题
|
||||
- 是否存在明显 ROI merge 问题
|
||||
|
||||
## 13. 实施时的关键注意点
|
||||
|
||||
### 13.1 单位统一
|
||||
|
||||
- `rot_y_decoded`、`xyzlhwyaw[6]` 为弧度
|
||||
- `xyzlhwyaw_ego[6]` 为角度
|
||||
|
||||
做差前必须统一单位。
|
||||
|
||||
### 13.2 不要直接看未 wrap 的 yaw 差值
|
||||
|
||||
所有时序稳定性指标都建议基于 wrap 后角度差计算。
|
||||
|
||||
### 13.3 `yaw_probs` 不要当 softmax 使用
|
||||
|
||||
它们是 4 个独立 sigmoid 分数,建议用:
|
||||
|
||||
- 最大值 `p1`
|
||||
- 次大值 `p2`
|
||||
- 差值 `margin`
|
||||
- 是否多个分数同时偏高
|
||||
|
||||
而不是看其和是否为 1。
|
||||
|
||||
### 13.4 merged 和 ROI 分支要分开分析
|
||||
|
||||
如果只看最终 merged 结果,容易把问题都归到模型本身。实际上很多问题可能来自:
|
||||
|
||||
- ROI 视角差异
|
||||
- ROI 边界裁切
|
||||
- cross-ROI NMS / merge 策略
|
||||
|
||||
### 13.5 先做统计,再做人工看图
|
||||
|
||||
不要一开始就逐条翻 case。先做分布和排序,效率更高。
|
||||
|
||||
## 14. 一句话结论
|
||||
|
||||
基于 `heading_debug` 的核心分析思路是:
|
||||
|
||||
- 先把 detection 扁平化并补齐 track / timestamp
|
||||
- 再用 wrap 后角度差做时序稳定性统计
|
||||
- 再结合 `yaw_bin`、`yaw_delta`、`margin`、`roi_id`、`face_cls` 做根因归类
|
||||
- 最后区分问题到底来自 bin 分类、delta 回归、角度表达、ROI 分支还是目标本身歧义
|
||||
366
tools/temporal_analysis/merge_tracking_results.py
Executable file
366
tools/temporal_analysis/merge_tracking_results.py
Executable file
@@ -0,0 +1,366 @@
|
||||
"""
|
||||
Merge multiple per-source tracking JSON files into a single combined JSON.
|
||||
|
||||
Each source (roi0, roi1, merge) is the output of track_objects.py. The script
|
||||
aligns frames by image_name, adds a "source" field to every detection, and
|
||||
writes a unified JSON where each frame contains detections from all sources.
|
||||
|
||||
Output format (list of frame dicts):
|
||||
[
|
||||
{
|
||||
"image_name": "1741824631_0",
|
||||
"detections": [
|
||||
{ "lane_assignment": 0, "track_id": 1, "class_id": 0, "bbox": [...], ... },
|
||||
{ "lane_assignment": 1, "track_id": 1, ... },
|
||||
{ "lane_assignment": 2, "track_id": 2, ... },
|
||||
...
|
||||
],
|
||||
"tracking_stats": {
|
||||
"roi0": { "total_tracks": 3, "active_tracks_by_class": {...} },
|
||||
"roi1": { ... },
|
||||
"merge": { ... }
|
||||
}
|
||||
},
|
||||
...
|
||||
]
|
||||
|
||||
Notes:
|
||||
- track_id values are offset per source so they are globally unique:
|
||||
roi0 keeps original IDs (0–9999), roi1 adds 10000, merge adds 20000.
|
||||
- lane_assignment values: 0=roi0, 1=roi1, 2=merge (positional index in source_map).
|
||||
- Missing sources are silently skipped (partial merge is valid).
|
||||
- Frames present in some sources but not others are still included, with
|
||||
detections only from the sources that have that frame.
|
||||
|
||||
Usage:
|
||||
python merge_tracking_results.py \
|
||||
--roi0 {case_dir}/roi0.json \
|
||||
--roi1 {case_dir}/roi1.json \
|
||||
--merge {case_dir}/merge.json \
|
||||
--output {case_dir}/combined_tracking.json
|
||||
|
||||
# Batch mode: scan all cases under results_root/model_name
|
||||
python merge_tracking_results.py \
|
||||
--results-root /data1/dongying/Mono3d/G1M3/cases_regular \
|
||||
--model-name model_20260228
|
||||
|
||||
# Custom source names (optional):
|
||||
python merge_tracking_results.py \
|
||||
--roi0 roi0.json --roi1 roi1.json --merge merge.json \
|
||||
--source-names roi0_track roi1_track merge_track \
|
||||
--output combined_tracking.json
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def normalize_image_name(name):
|
||||
"""Normalize image_name to a canonical form for cross-source frame alignment.
|
||||
|
||||
merge_json filenames are saved as ``{saved_count:06d}_{img_name}_merged.json``
|
||||
while roi0/roi1 use plain ``{img_name}.json``. This function strips both
|
||||
the leading 6-digit counter prefix and the trailing ``_merged`` suffix so
|
||||
all three sources resolve to the same key.
|
||||
|
||||
Examples::
|
||||
|
||||
'000001_1741824631_0_merged' -> '1741824631_0'
|
||||
'1741824631_0' -> '1741824631_0' (unchanged)
|
||||
"""
|
||||
name = re.sub(r'^\d{6}_', '', name) # strip leading counter, e.g. '000001_'
|
||||
name = re.sub(r'_merged$', '', name) # strip trailing '_merged'
|
||||
return name
|
||||
|
||||
|
||||
def load_tracking_json(path, verbose=True):
|
||||
"""Load a tracking JSON file produced by track_objects.py.
|
||||
|
||||
Args:
|
||||
path: Path to the JSON file (list of frame dicts).
|
||||
|
||||
Returns:
|
||||
List of frame dicts, or empty list if file does not exist / is invalid.
|
||||
"""
|
||||
p = Path(path)
|
||||
if not p.exists():
|
||||
if verbose:
|
||||
print(f"Warning: tracking file not found, skipping: {p}")
|
||||
return []
|
||||
with open(p, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
if not isinstance(data, list):
|
||||
if verbose:
|
||||
print(f"Warning: expected a list in {p}, got {type(data).__name__}. Skipping.")
|
||||
return []
|
||||
if verbose:
|
||||
print(f"Loaded {len(data)} frame(s) from {p}")
|
||||
return data
|
||||
|
||||
|
||||
def build_source_map(roi0_path, roi1_path, merge_path, source_names, extra_sources=None, verbose=True):
|
||||
"""Load all available tracking inputs for a single case."""
|
||||
roi0_name, roi1_name, merge_name = source_names
|
||||
|
||||
source_map = []
|
||||
if roi0_path is not None:
|
||||
frames = load_tracking_json(roi0_path, verbose=verbose)
|
||||
if frames:
|
||||
source_map.append((roi0_name, frames))
|
||||
if roi1_path is not None:
|
||||
frames = load_tracking_json(roi1_path, verbose=verbose)
|
||||
if frames:
|
||||
source_map.append((roi1_name, frames))
|
||||
if merge_path is not None:
|
||||
frames = load_tracking_json(merge_path, verbose=verbose)
|
||||
if frames:
|
||||
source_map.append((merge_name, frames))
|
||||
for source_name, source_path in extra_sources or []:
|
||||
frames = load_tracking_json(source_path, verbose=verbose)
|
||||
if frames:
|
||||
source_map.append((source_name, frames))
|
||||
|
||||
return source_map
|
||||
|
||||
|
||||
# Offset added to track_id per source so IDs are globally unique:
|
||||
# source_idx 0 (roi0) -> no offset
|
||||
# source_idx 1 (roi1) -> +10000
|
||||
# source_idx 2 (merge) -> +20000
|
||||
# source_idx 3+ (extra src) -> keep incrementing by +10000
|
||||
TRACK_ID_OFFSET_PER_SOURCE = 10000
|
||||
|
||||
|
||||
def merge_tracking_results(source_map):
|
||||
"""Merge tracking results from multiple sources into a single per-frame list.
|
||||
|
||||
Args:
|
||||
source_map: Ordered dict / list of (source_name, frames_list) pairs.
|
||||
Each frames_list is the output of track_objects.py.
|
||||
|
||||
Returns:
|
||||
List of merged frame dicts, one per unique image_name found across all
|
||||
sources. Frames are ordered by the first source that contains them;
|
||||
remaining sources are appended in order.
|
||||
"""
|
||||
# Build per-source index: image_name -> frame_dict
|
||||
source_indices = {}
|
||||
all_image_names_ordered = []
|
||||
seen_names = set()
|
||||
|
||||
for source_name, frames in source_map:
|
||||
idx = {}
|
||||
for frame in frames:
|
||||
name = normalize_image_name(frame.get('image_name') or '')
|
||||
idx[name] = frame
|
||||
if name not in seen_names:
|
||||
all_image_names_ordered.append(name)
|
||||
seen_names.add(name)
|
||||
source_indices[source_name] = idx
|
||||
|
||||
merged_frames = []
|
||||
for image_name in all_image_names_ordered:
|
||||
merged_detections = []
|
||||
merged_stats = {}
|
||||
|
||||
for source_idx, (source_name, _) in enumerate(source_map):
|
||||
frame = source_indices[source_name].get(image_name)
|
||||
if frame is None:
|
||||
continue
|
||||
|
||||
# Tag every detection with its lane_assignment (0=roi0, 1=roi1, 2=merge)
|
||||
# and offset track_id so it is globally unique across sources.
|
||||
id_offset = source_idx * TRACK_ID_OFFSET_PER_SOURCE
|
||||
for det in frame.get('detections', []):
|
||||
tagged = dict(det)
|
||||
tagged['lane_assignment'] = source_idx
|
||||
if 'track_id' in tagged and tagged['track_id'] is not None:
|
||||
tagged['track_id'] = tagged['track_id'] + id_offset
|
||||
merged_detections.append(tagged)
|
||||
|
||||
# Collect per-source tracking_stats
|
||||
if 'tracking_stats' in frame:
|
||||
merged_stats[source_name] = frame['tracking_stats']
|
||||
|
||||
merged_frame = {
|
||||
'image_name': image_name,
|
||||
'detections': merged_detections,
|
||||
}
|
||||
if merged_stats:
|
||||
merged_frame['tracking_stats'] = merged_stats
|
||||
|
||||
merged_frames.append(merged_frame)
|
||||
|
||||
return merged_frames
|
||||
|
||||
|
||||
def write_merged_output(merged, output_path):
|
||||
"""Persist merged frames to disk."""
|
||||
output_path = Path(output_path)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(merged, f, indent=2, ensure_ascii=False)
|
||||
|
||||
|
||||
def print_merge_summary(merged, source_map, output_path):
|
||||
"""Print aggregate detection counts per source for one merged output."""
|
||||
idx_to_name = {i: s for i, (s, _) in enumerate(source_map)}
|
||||
total_by_source = {s: 0 for s, _ in source_map}
|
||||
for frame in merged:
|
||||
for det in frame.get('detections', []):
|
||||
lane = det.get('lane_assignment')
|
||||
src = idx_to_name.get(lane)
|
||||
if src is not None:
|
||||
total_by_source[src] += 1
|
||||
|
||||
print(f"\nMerge complete: {len(merged)} frame(s) written to {output_path}")
|
||||
for i, (src, _) in enumerate(source_map):
|
||||
print(f" lane_assignment={i} ({src}): {total_by_source[src]} detection(s) total")
|
||||
|
||||
|
||||
def merge_case(roi0_path, roi1_path, merge_path, output_path, source_names, extra_sources=None, verbose=True):
|
||||
"""Merge one case from explicit input paths."""
|
||||
source_map = build_source_map(
|
||||
roi0_path,
|
||||
roi1_path,
|
||||
merge_path,
|
||||
source_names,
|
||||
extra_sources=extra_sources,
|
||||
verbose=verbose,
|
||||
)
|
||||
if not source_map:
|
||||
if verbose:
|
||||
print("Error: No valid tracking files were loaded. Nothing to merge.")
|
||||
return False
|
||||
|
||||
if verbose:
|
||||
print(f"Merging {len(source_map)} source(s): {[s for s, _ in source_map]}")
|
||||
merged = merge_tracking_results(source_map)
|
||||
write_merged_output(merged, output_path)
|
||||
if verbose:
|
||||
print_merge_summary(merged, source_map, output_path)
|
||||
return True
|
||||
|
||||
|
||||
def find_case_dirs(results_root, model_name):
|
||||
"""Return all case directories that contain roi1.json under results_root/model_name."""
|
||||
search_root = Path(results_root) / model_name
|
||||
if not search_root.exists():
|
||||
print(f"Error: model directory does not exist: {search_root}")
|
||||
return []
|
||||
|
||||
return sorted(path.parent for path in search_root.rglob('roi1.json'))
|
||||
|
||||
|
||||
def merge_cases_in_batch(results_root, model_name, source_names, output_name):
|
||||
"""Scan case directories in Python and merge each one sequentially."""
|
||||
case_dirs = find_case_dirs(results_root, model_name)
|
||||
if not case_dirs:
|
||||
print("Error: No roi1.json files were found. Nothing to merge.")
|
||||
return False
|
||||
|
||||
print(f"Found {len(case_dirs)} case(s) under {Path(results_root) / model_name}")
|
||||
merged_cases = 0
|
||||
failed_cases = 0
|
||||
|
||||
for case_dir in case_dirs:
|
||||
print("")
|
||||
print("==========================================")
|
||||
print(f"Case : {case_dir}")
|
||||
print(f"Output : {case_dir / output_name}")
|
||||
print("==========================================")
|
||||
|
||||
ok = merge_case(
|
||||
roi0_path=case_dir / 'roi0.json',
|
||||
roi1_path=case_dir / 'roi1.json',
|
||||
merge_path=case_dir / 'merge.json',
|
||||
output_path=case_dir / output_name,
|
||||
source_names=source_names,
|
||||
)
|
||||
if ok:
|
||||
merged_cases += 1
|
||||
else:
|
||||
failed_cases += 1
|
||||
|
||||
print("")
|
||||
print("Batch merge complete")
|
||||
print(f" merged_cases: {merged_cases}")
|
||||
print(f" failed_cases: {failed_cases}")
|
||||
return failed_cases == 0
|
||||
|
||||
|
||||
def parse_extra_source_specs(specs):
|
||||
"""Parse CLI specs of the form NAME=PATH for additional tracking sources."""
|
||||
parsed = []
|
||||
for spec in specs or []:
|
||||
if '=' not in spec:
|
||||
raise ValueError(f"Invalid --extra-source {spec!r}; expected NAME=PATH")
|
||||
source_name, source_path = spec.split('=', 1)
|
||||
source_name = source_name.strip()
|
||||
source_path = source_path.strip()
|
||||
if not source_name or not source_path:
|
||||
raise ValueError(f"Invalid --extra-source {spec!r}; expected NAME=PATH")
|
||||
parsed.append((source_name, source_path))
|
||||
return parsed
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Merge per-source tracking JSONs into a single combined JSON.'
|
||||
)
|
||||
parser.add_argument('--roi0', type=str, default=None,
|
||||
help='Path to ROI0 tracking JSON (output of track_objects.py)')
|
||||
parser.add_argument('--roi1', type=str, default=None,
|
||||
help='Path to ROI1 tracking JSON (output of track_objects.py)')
|
||||
parser.add_argument('--merge', type=str, default=None,
|
||||
help='Path to merged-ROI tracking JSON (output of track_objects.py)')
|
||||
parser.add_argument('--results-root', type=str, default=None,
|
||||
help='Batch mode root directory that contains per-model case results')
|
||||
parser.add_argument('--model-name', type=str, default=None,
|
||||
help='Batch mode model directory name under results-root')
|
||||
parser.add_argument('--output-name', type=str, default='combined_tracking.json',
|
||||
help='Batch mode output filename written inside each case directory')
|
||||
parser.add_argument('--source-names', type=str, nargs=3,
|
||||
default=['roi0', 'roi1', 'merge'],
|
||||
metavar=('ROI0_NAME', 'ROI1_NAME', 'MERGE_NAME'),
|
||||
help='Custom labels for the three sources (default: roi0 roi1 merge)')
|
||||
parser.add_argument('--extra-source', type=str, action='append', default=[],
|
||||
metavar='NAME=PATH',
|
||||
help='Additional tracking source to append in single-case mode. Can be passed multiple times.')
|
||||
parser.add_argument('--output', type=str, default=None,
|
||||
help='Output path for the combined tracking JSON in single-case mode')
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.results_root or args.model_name:
|
||||
if not args.results_root or not args.model_name:
|
||||
parser.error('--results-root and --model-name must be provided together in batch mode')
|
||||
merge_cases_in_batch(
|
||||
results_root=args.results_root,
|
||||
model_name=args.model_name,
|
||||
source_names=args.source_names,
|
||||
output_name=args.output_name,
|
||||
)
|
||||
return
|
||||
|
||||
if args.output is None:
|
||||
parser.error('--output is required in single-case mode')
|
||||
|
||||
try:
|
||||
extra_sources = parse_extra_source_specs(args.extra_source)
|
||||
except ValueError as exc:
|
||||
parser.error(str(exc))
|
||||
|
||||
merge_case(
|
||||
roi0_path=args.roi0,
|
||||
roi1_path=args.roi1,
|
||||
merge_path=args.merge,
|
||||
output_path=args.output,
|
||||
source_names=args.source_names,
|
||||
extra_sources=extra_sources,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
21
tools/temporal_analysis/merge_tracking_results.sh
Executable file
21
tools/temporal_analysis/merge_tracking_results.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
# 将三路跟踪结果(roi0.json / roi1.json / merge.json)合并为 combined_tracking.json
|
||||
# 前提:已运行 track_objects.sh 生成上述三个 JSON 文件
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
export PYTHONPATH="${PROJECT_ROOT}:${PYTHONPATH}"
|
||||
|
||||
RESULTS_ROOT="/data1/dongying/Mono3d/G1M3/cases_regular"
|
||||
MODEL_NAME="model_20260228"
|
||||
|
||||
# python "${PROJECT_ROOT}/tools/temporal_analysis/merge_tracking_results.py" \
|
||||
# --results-root "${RESULTS_ROOT}" \
|
||||
# --model-name "${MODEL_NAME}"
|
||||
|
||||
python "${PROJECT_ROOT}/tools/temporal_analysis/merge_tracking_results.py" \
|
||||
--roi0 /data1/dongying/Mono3d/G1M3/cases_regular/model_20260228_test/roi0.json \
|
||||
--roi1 /data1/dongying/Mono3d/G1M3/cases_regular/model_20260228_test/roi1.json \
|
||||
--merge /data1/dongying/Mono3d/G1M3/cases_regular/model_20260228_test/merge.json \
|
||||
--output-name combined_tracking.json \
|
||||
--output /data1/dongying/Mono3d/G1M3/cases_regular/model_20260228_test/combined_tracking.json
|
||||
340
tools/temporal_analysis/plot_track_range_2d_correlations.py
Executable file
340
tools/temporal_analysis/plot_track_range_2d_correlations.py
Executable file
@@ -0,0 +1,340 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Plot range-vs-2D-feature correlation figures for one tracked object."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib
|
||||
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
|
||||
DEFAULT_FEATURES = ("h", "area", "y2")
|
||||
FEATURE_LABELS = {
|
||||
"x_ego": "X Ego (m)",
|
||||
"w": "BBox Width (px)",
|
||||
"h": "BBox Height (px)",
|
||||
"area": "BBox Area (px^2)",
|
||||
"x1": "BBox x1 (px)",
|
||||
"x2": "BBox x2 (px)",
|
||||
"y1": "BBox y1 (px)",
|
||||
"y2": "BBox y2 (px)",
|
||||
"cx": "BBox cx (px)",
|
||||
"cy": "BBox cy (px)",
|
||||
"aspect": "BBox Aspect",
|
||||
}
|
||||
FEATURE_COLORS = {
|
||||
"x_ego": "#1d4ed8",
|
||||
"h": "#059669",
|
||||
"area": "#b45309",
|
||||
"y2": "#b91c1c",
|
||||
"y1": "#7c3aed",
|
||||
"cy": "#db2777",
|
||||
"w": "#0f766e",
|
||||
"x1": "#475569",
|
||||
"x2": "#9333ea",
|
||||
"cx": "#ea580c",
|
||||
"aspect": "#0891b2",
|
||||
}
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--tracking", required=True, type=Path, help="Tracking JSON, e.g. merge.json")
|
||||
parser.add_argument("--series-csv", required=True, type=Path, help="Per-track temporal observation CSV")
|
||||
parser.add_argument("--track-id", required=True, type=int)
|
||||
parser.add_argument("--output-dir", required=True, type=Path)
|
||||
parser.add_argument(
|
||||
"--feature",
|
||||
action="append",
|
||||
dest="features",
|
||||
help="2D feature to include. Can repeat. Defaults to h, area, y2.",
|
||||
)
|
||||
parser.add_argument("--smooth-window", type=int, default=21)
|
||||
parser.add_argument("--max-lag", type=int, default=20)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def load_series_csv(path: Path) -> list[dict]:
|
||||
rows = list(csv.DictReader(path.open()))
|
||||
for row in rows:
|
||||
row["frame_id"] = int(row["frame_id"])
|
||||
row["frame_idx"] = int(row["frame_idx"])
|
||||
row["x_ego"] = float(row["x_ego"])
|
||||
return rows
|
||||
|
||||
|
||||
def load_track_feature_series(tracking_path: Path, frame_ids: set[int], track_id: int) -> list[dict]:
|
||||
frames = json.loads(tracking_path.read_text())
|
||||
out = []
|
||||
for frame in frames:
|
||||
det = next((item for item in frame.get("detections", []) if int(item.get("track_id", -1)) == track_id), None)
|
||||
if det is None:
|
||||
continue
|
||||
frame_id = int(det.get("frameId", det.get("frame_id")))
|
||||
if frame_id not in frame_ids:
|
||||
continue
|
||||
x1, y1, x2, y2 = map(float, det["bbox"])
|
||||
w = x2 - x1
|
||||
h = y2 - y1
|
||||
out.append(
|
||||
{
|
||||
"frame_id": frame_id,
|
||||
"x1": x1,
|
||||
"y1": y1,
|
||||
"x2": x2,
|
||||
"y2": y2,
|
||||
"cx": (x1 + x2) * 0.5,
|
||||
"cy": (y1 + y2) * 0.5,
|
||||
"w": w,
|
||||
"h": h,
|
||||
"area": w * h,
|
||||
"aspect": w / h if h else 0.0,
|
||||
}
|
||||
)
|
||||
out.sort(key=lambda item: item["frame_id"])
|
||||
return out
|
||||
|
||||
|
||||
def moving_average(values: np.ndarray, window: int) -> np.ndarray:
|
||||
if window <= 1:
|
||||
return values.copy()
|
||||
pad = window // 2
|
||||
padded = np.pad(values, (pad, pad), mode="edge")
|
||||
kernel = np.ones(window, dtype=np.float64) / window
|
||||
return np.convolve(padded, kernel, mode="valid")
|
||||
|
||||
|
||||
def detrend(values: np.ndarray) -> np.ndarray:
|
||||
x = np.arange(len(values), dtype=np.float64)
|
||||
coef = np.polyfit(x, values, 1)
|
||||
return values - np.polyval(coef, x)
|
||||
|
||||
|
||||
def zscore(values: np.ndarray) -> np.ndarray:
|
||||
std = float(np.std(values))
|
||||
if std == 0.0:
|
||||
return np.zeros_like(values, dtype=np.float64)
|
||||
return (values - float(np.mean(values))) / std
|
||||
|
||||
|
||||
def corr(a: np.ndarray, b: np.ndarray) -> float:
|
||||
a = a.astype(np.float64)
|
||||
b = b.astype(np.float64)
|
||||
a = a - a.mean()
|
||||
b = b - b.mean()
|
||||
denom = float(np.sqrt(np.dot(a, a) * np.dot(b, b)))
|
||||
if denom == 0.0:
|
||||
return 0.0
|
||||
return float(np.dot(a, b) / denom)
|
||||
|
||||
|
||||
def build_arrays(series_rows: list[dict], feature_rows: list[dict], features: list[str]) -> dict[str, np.ndarray]:
|
||||
feature_map = {row["frame_id"]: row for row in feature_rows}
|
||||
frame_ids = [row["frame_id"] for row in series_rows if row["frame_id"] in feature_map]
|
||||
result = {
|
||||
"frame_id": np.asarray(frame_ids, dtype=np.int64),
|
||||
"x_ego": np.asarray([row["x_ego"] for row in series_rows if row["frame_id"] in feature_map], dtype=np.float64),
|
||||
}
|
||||
for feat in features:
|
||||
result[feat] = np.asarray([feature_map[fid][feat] for fid in frame_ids], dtype=np.float64)
|
||||
return result
|
||||
|
||||
|
||||
def save_summary(path: Path, payload: dict) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def plot_raw_curves(arrays: dict[str, np.ndarray], features: list[str], output_path: Path) -> None:
|
||||
frame_id = arrays["frame_id"]
|
||||
fig, axes = plt.subplots(len(features) + 1, 1, figsize=(14, 3.2 * (len(features) + 1)), sharex=True)
|
||||
if len(features) == 0:
|
||||
axes = [axes]
|
||||
|
||||
axes[0].plot(frame_id, arrays["x_ego"], color=FEATURE_COLORS["x_ego"], linewidth=1.4)
|
||||
axes[0].set_ylabel(FEATURE_LABELS["x_ego"])
|
||||
axes[0].set_title("Raw Curves")
|
||||
axes[0].grid(True, alpha=0.3)
|
||||
|
||||
for idx, feat in enumerate(features, start=1):
|
||||
axes[idx].plot(frame_id, arrays[feat], color=FEATURE_COLORS.get(feat, "#374151"), linewidth=1.2)
|
||||
axes[idx].set_ylabel(FEATURE_LABELS.get(feat, feat))
|
||||
axes[idx].grid(True, alpha=0.3)
|
||||
|
||||
axes[-1].set_xlabel("Frame ID")
|
||||
fig.tight_layout()
|
||||
fig.savefig(output_path, dpi=150)
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def plot_smoothed_detrended_overlay(
|
||||
arrays: dict[str, np.ndarray],
|
||||
features: list[str],
|
||||
smooth_window: int,
|
||||
output_path: Path,
|
||||
) -> dict[str, np.ndarray]:
|
||||
frame_id = arrays["frame_id"]
|
||||
processed = {}
|
||||
fig, ax = plt.subplots(figsize=(14, 6))
|
||||
|
||||
x_vals = zscore(detrend(moving_average(arrays["x_ego"], smooth_window)))
|
||||
processed["x_ego"] = x_vals
|
||||
ax.plot(frame_id, x_vals, color=FEATURE_COLORS["x_ego"], linewidth=2.0, label="x_ego")
|
||||
|
||||
for feat in features:
|
||||
vals = zscore(detrend(moving_average(arrays[feat], smooth_window)))
|
||||
processed[feat] = vals
|
||||
ax.plot(frame_id, vals, linewidth=1.6, label=feat, color=FEATURE_COLORS.get(feat, "#374151"))
|
||||
|
||||
ax.set_title(f"Smoothed + Detrended + Z-scored Curves (window={smooth_window})")
|
||||
ax.set_xlabel("Frame ID")
|
||||
ax.set_ylabel("Z-score")
|
||||
ax.grid(True, alpha=0.3)
|
||||
ax.legend()
|
||||
fig.tight_layout()
|
||||
fig.savefig(output_path, dpi=150)
|
||||
plt.close(fig)
|
||||
return processed
|
||||
|
||||
|
||||
def plot_delta_overlay(arrays: dict[str, np.ndarray], features: list[str], output_path: Path) -> dict[str, np.ndarray]:
|
||||
frame_id = arrays["frame_id"][1:]
|
||||
processed = {}
|
||||
fig, ax = plt.subplots(figsize=(14, 6))
|
||||
|
||||
dx = np.diff(arrays["x_ego"])
|
||||
processed["x_ego"] = dx
|
||||
ax.plot(frame_id, zscore(dx), color=FEATURE_COLORS["x_ego"], linewidth=2.0, label="Δx_ego")
|
||||
|
||||
for feat in features:
|
||||
vals = np.diff(arrays[feat])
|
||||
processed[feat] = vals
|
||||
ax.plot(frame_id, zscore(vals), linewidth=1.4, label=f"Δ{feat}", color=FEATURE_COLORS.get(feat, "#374151"))
|
||||
|
||||
ax.set_title("Frame-to-Frame Delta Curves (Z-scored)")
|
||||
ax.set_xlabel("Frame ID")
|
||||
ax.set_ylabel("Z-score")
|
||||
ax.grid(True, alpha=0.3)
|
||||
ax.legend()
|
||||
fig.tight_layout()
|
||||
fig.savefig(output_path, dpi=150)
|
||||
plt.close(fig)
|
||||
return processed
|
||||
|
||||
|
||||
def plot_delta_scatter(delta_arrays: dict[str, np.ndarray], features: list[str], output_path: Path) -> dict[str, dict[str, float]]:
|
||||
dx = delta_arrays["x_ego"]
|
||||
fig, axes = plt.subplots(1, len(features), figsize=(5.3 * len(features), 4.6))
|
||||
if len(features) == 1:
|
||||
axes = [axes]
|
||||
|
||||
summary = {}
|
||||
for ax, feat in zip(axes, features):
|
||||
vals = delta_arrays[feat]
|
||||
signed = corr(dx, vals)
|
||||
magnitude = corr(np.abs(dx), np.abs(vals))
|
||||
ax.scatter(vals, dx, s=12, alpha=0.55, color=FEATURE_COLORS.get(feat, "#374151"))
|
||||
ax.set_xlabel(f"Δ{feat}")
|
||||
ax.set_ylabel("Δx_ego")
|
||||
ax.set_title(f"{feat}\nr={signed:.3f}, |r|={magnitude:.3f}")
|
||||
ax.grid(True, alpha=0.25)
|
||||
summary[feat] = {
|
||||
"corr_signed": signed,
|
||||
"corr_magnitude": magnitude,
|
||||
}
|
||||
|
||||
fig.tight_layout()
|
||||
fig.savefig(output_path, dpi=150)
|
||||
plt.close(fig)
|
||||
return summary
|
||||
|
||||
|
||||
def plot_cross_correlation(
|
||||
processed: dict[str, np.ndarray],
|
||||
features: list[str],
|
||||
max_lag: int,
|
||||
output_path: Path,
|
||||
) -> dict[str, list[dict[str, float]]]:
|
||||
ref = processed["x_ego"]
|
||||
fig, ax = plt.subplots(figsize=(14, 6))
|
||||
summary = {}
|
||||
|
||||
for feat in features:
|
||||
vals = processed[feat]
|
||||
points = []
|
||||
for lag in range(-max_lag, max_lag + 1):
|
||||
if lag < 0:
|
||||
corr_value = corr(ref[-lag:], vals[:lag])
|
||||
elif lag > 0:
|
||||
corr_value = corr(ref[:-lag], vals[lag:])
|
||||
else:
|
||||
corr_value = corr(ref, vals)
|
||||
points.append({"lag": lag, "corr": corr_value})
|
||||
summary[feat] = points
|
||||
ax.plot([item["lag"] for item in points], [item["corr"] for item in points], label=feat, linewidth=1.8)
|
||||
|
||||
ax.axhline(0.0, color="#6b7280", linestyle="--", linewidth=1)
|
||||
ax.set_title("Cross-correlation of Low-frequency Components")
|
||||
ax.set_xlabel("Lag (frames)")
|
||||
ax.set_ylabel("Correlation")
|
||||
ax.grid(True, alpha=0.3)
|
||||
ax.legend()
|
||||
fig.tight_layout()
|
||||
fig.savefig(output_path, dpi=150)
|
||||
plt.close(fig)
|
||||
return summary
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
features = args.features or list(DEFAULT_FEATURES)
|
||||
tracking_path = args.tracking.resolve()
|
||||
series_csv = args.series_csv.resolve()
|
||||
output_dir = args.output_dir.resolve()
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
series_rows = load_series_csv(series_csv)
|
||||
frame_ids = {row["frame_id"] for row in series_rows}
|
||||
feature_rows = load_track_feature_series(tracking_path, frame_ids, args.track_id)
|
||||
arrays = build_arrays(series_rows, feature_rows, features)
|
||||
|
||||
plot_raw_curves(arrays, features, output_dir / "raw_curves.png")
|
||||
smooth_processed = plot_smoothed_detrended_overlay(
|
||||
arrays,
|
||||
features,
|
||||
smooth_window=args.smooth_window,
|
||||
output_path=output_dir / "smoothed_detrended_zscore_overlay.png",
|
||||
)
|
||||
delta_arrays = plot_delta_overlay(arrays, features, output_dir / "delta_zscore_overlay.png")
|
||||
scatter_summary = plot_delta_scatter(delta_arrays, features, output_dir / "delta_scatter.png")
|
||||
cross_corr_summary = plot_cross_correlation(
|
||||
smooth_processed,
|
||||
features,
|
||||
max_lag=args.max_lag,
|
||||
output_path=output_dir / "cross_correlation.png",
|
||||
)
|
||||
|
||||
summary = {
|
||||
"track_id": args.track_id,
|
||||
"features": features,
|
||||
"tracking_json": str(tracking_path),
|
||||
"series_csv": str(series_csv),
|
||||
"smooth_window": args.smooth_window,
|
||||
"max_lag": args.max_lag,
|
||||
"scatter_summary": scatter_summary,
|
||||
"cross_correlation": cross_corr_summary,
|
||||
}
|
||||
save_summary(output_dir / "summary.json", summary)
|
||||
print(output_dir)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
25
tools/temporal_analysis/run_analyze_heading_debug.sh
Executable file
25
tools/temporal_analysis/run_analyze_heading_debug.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
# 简洁启动 analyze_heading_debug.py 的脚本
|
||||
# 用法示例:
|
||||
# bash run_analyze_heading_debug.sh /path/to/tracking.json
|
||||
# bash run_analyze_heading_debug.sh /path/to/case_dir combined_tracking.json
|
||||
|
||||
# /data1/dongying/Mono3d/G1M3/pdcl_batch_infer/cases_aeb_v2/CPFA/25_6.5_20/019d2d81-68fc-7b69-45fe-930341ffd56e/merge.json
|
||||
# /data1/dongying/Mono3d/G1M3/pdcl_batch_infer/cases_aeb_v2/CBNA/50_15_50/019d2d86-4a7b-72da-6e09-1e7ab3494d0b/merge.json
|
||||
|
||||
INPUT_PATH="$1"
|
||||
JSON_NAME="$2"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PYTHON_SCRIPT="$SCRIPT_DIR/analyze_heading_debug.py"
|
||||
|
||||
if [ -z "$INPUT_PATH" ]; then
|
||||
echo "用法: bash run_analyze_heading_debug.sh <input_path> [json_name]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$JSON_NAME" ]; then
|
||||
python "$PYTHON_SCRIPT" --input "$INPUT_PATH" --class-ids 1 3 --large-dyaw 0.78 --large-delta 0.78
|
||||
else
|
||||
python "$PYTHON_SCRIPT" --input "$INPUT_PATH" --json-name "$JSON_NAME" --class-ids 1 3 --large-dyaw 0.78 --large-delta 0.78
|
||||
fi
|
||||
107
tools/temporal_analysis/run_analyze_heading_debug_batch.sh
Executable file
107
tools/temporal_analysis/run_analyze_heading_debug_batch.sh
Executable file
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
shopt -s nullglob
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
PYTHON_BIN="${PYTHON_BIN:-/deeplearning_team/ydong/dongying/miniconda/envs/yolov5/bin/python}"
|
||||
|
||||
CASE_ROOT="${CASE_ROOT:-/data1/dongying/Mono3d/G1M3/pdcl_batch_infer/cases_aeb_v2}"
|
||||
|
||||
SCENE_FILTER="${SCENE_FILTER:-}"
|
||||
TEST_FILTER="${TEST_FILTER:-}"
|
||||
CASE_FILTER="${CASE_FILTER:-}"
|
||||
DRY_RUN="${DRY_RUN:-0}"
|
||||
STOP_ON_ERROR="${STOP_ON_ERROR:-0}"
|
||||
|
||||
FORWARD_ARGS=("$@")
|
||||
|
||||
run_one_case() {
|
||||
local scene_name="$1"
|
||||
local test_name="$2"
|
||||
local case_dir="$3"
|
||||
local case_name
|
||||
case_name="$(basename "$case_dir")"
|
||||
|
||||
local cmd=(
|
||||
"$PYTHON_BIN"
|
||||
"tools/temporal_analysis/analyze_heading_debug.py"
|
||||
--input "$case_dir"
|
||||
"${FORWARD_ARGS[@]}"
|
||||
)
|
||||
|
||||
printf '[RUN] %s / %s / %s\n' "$scene_name" "$test_name" "$case_name"
|
||||
|
||||
if [[ "$DRY_RUN" == "1" ]]; then
|
||||
printf ' '
|
||||
printf '%q ' "${cmd[@]}"
|
||||
printf '\n'
|
||||
return 0
|
||||
fi
|
||||
|
||||
(
|
||||
cd "$REPO_ROOT"
|
||||
"${cmd[@]}"
|
||||
)
|
||||
}
|
||||
|
||||
if [[ ! -d "$CASE_ROOT" ]]; then
|
||||
echo "[ERROR] CASE_ROOT not found: $CASE_ROOT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
total=0
|
||||
success=0
|
||||
failed=0
|
||||
|
||||
for scene_dir in "$CASE_ROOT"/*; do
|
||||
[[ -d "$scene_dir" ]] || continue
|
||||
scene_name="$(basename "$scene_dir")"
|
||||
|
||||
if [[ -n "$SCENE_FILTER" ]] && [[ ! "$scene_name" =~ $SCENE_FILTER ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
for test_dir in "$scene_dir"/*; do
|
||||
[[ -d "$test_dir" ]] || continue
|
||||
test_name="$(basename "$test_dir")"
|
||||
|
||||
if [[ -n "$TEST_FILTER" ]] && [[ ! "$test_name" =~ $TEST_FILTER ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
found_case=0
|
||||
for case_dir in "$test_dir"/*; do
|
||||
[[ -d "$case_dir" ]] || continue
|
||||
case_name="$(basename "$case_dir")"
|
||||
|
||||
if [[ -n "$CASE_FILTER" ]] && [[ ! "$case_name" =~ $CASE_FILTER ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
found_case=1
|
||||
((total+=1))
|
||||
|
||||
if run_one_case "$scene_name" "$test_name" "$case_dir"; then
|
||||
((success+=1))
|
||||
else
|
||||
((failed+=1))
|
||||
printf '[FAIL] %s / %s / %s\n' \
|
||||
"$scene_name" "$test_name" "$case_name" >&2
|
||||
if [[ "$STOP_ON_ERROR" == "1" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$found_case" == "0" ]]; then
|
||||
printf '[SKIP] empty test dir: %s / %s\n' "$scene_name" "$test_name"
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
printf '\n[DONE] total=%d success=%d failed=%d\n' "$total" "$success" "$failed"
|
||||
|
||||
if [[ "$failed" -gt 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
239
tools/temporal_analysis/split_tracking_json.py
Executable file
239
tools/temporal_analysis/split_tracking_json.py
Executable file
@@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Split tracking JSON into one JSON file per frame while preserving track IDs.
|
||||
|
||||
Input format:
|
||||
[
|
||||
{
|
||||
"image_name": "1741824631_0",
|
||||
"detections": [
|
||||
{
|
||||
"class_id": 0,
|
||||
"confidence": 0.91,
|
||||
"bbox": [x1, y1, x2, y2],
|
||||
"track_id": 7,
|
||||
...
|
||||
}
|
||||
]
|
||||
},
|
||||
...
|
||||
]
|
||||
|
||||
Output format (one file per frame, evaluator-friendly):
|
||||
{
|
||||
"0": {
|
||||
"type": "0",
|
||||
"type_name": "vehicle",
|
||||
"score": "0.91",
|
||||
"box2d": [x1, y1, x2, y2],
|
||||
"xyzlhwyaw": [...],
|
||||
"face_cls": "front",
|
||||
"cut_cls": "0",
|
||||
"cut_cls_name": "nocut",
|
||||
"track_id": 7
|
||||
},
|
||||
"1": { ... }
|
||||
}
|
||||
|
||||
Usage:
|
||||
python tools/temporal_analysis/split_tracking_json.py \
|
||||
--input /path/to/roi0.json \
|
||||
--output-dir /path/to/json_results
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from merge_tracking_results import normalize_image_name
|
||||
|
||||
|
||||
CLASS_ID_TO_NAME = {
|
||||
0: "vehicle",
|
||||
1: "pedestrian",
|
||||
2: "bicycle",
|
||||
3: "rider",
|
||||
4: "roadblock",
|
||||
5: "head",
|
||||
6: "tsr",
|
||||
7: "guideboard",
|
||||
8: "plate",
|
||||
9: "wheel",
|
||||
10: "tl_border",
|
||||
11: "tl_wick",
|
||||
12: "tl_num",
|
||||
13: "tricycle",
|
||||
}
|
||||
|
||||
|
||||
def format_scalar(value):
|
||||
"""Keep ints as ints, convert everything else to plain JSON numbers/strings."""
|
||||
if value is None:
|
||||
return value
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
if isinstance(value, float):
|
||||
return float(value)
|
||||
return value
|
||||
|
||||
|
||||
def normalize_detection(det, keep_all_fields=False):
|
||||
"""Convert one tracking detection to evaluator-friendly single-frame JSON schema."""
|
||||
class_id = int(det.get("class_id", -1))
|
||||
bbox = det.get("bbox", [])
|
||||
confidence = det.get("confidence", 0.0)
|
||||
|
||||
item = {
|
||||
"type": str(class_id),
|
||||
"type_name": det.get("type_name") or CLASS_ID_TO_NAME.get(class_id, ""),
|
||||
"score": confidence,
|
||||
"box2d": [format_scalar(v) for v in bbox[:4]],
|
||||
"face_cls": det.get("face_cls", "none"),
|
||||
"cut_cls": str(det.get("cut_cls", -1)),
|
||||
"cut_cls_name": det.get("cut_cls_name", "none"),
|
||||
}
|
||||
|
||||
if "track_id" in det and det.get("track_id") is not None:
|
||||
item["track_id"] = int(det["track_id"])
|
||||
if "lane_assignment" in det and det.get("lane_assignment") is not None:
|
||||
item["lane_assignment"] = int(det["lane_assignment"])
|
||||
if "frameId" in det and det.get("frameId") is not None:
|
||||
item["frameId"] = det["frameId"]
|
||||
if "timestamp" in det and det.get("timestamp") is not None:
|
||||
item["timestamp"] = format_scalar(det["timestamp"])
|
||||
if "version" in det and det.get("version") is not None:
|
||||
item["version"] = det["version"]
|
||||
if "anchor" in det and det.get("anchor") is not None:
|
||||
item["anchor"] = det["anchor"]
|
||||
|
||||
if "roi_id" in det and det.get("roi_id") is not None:
|
||||
item["roi_id"] = int(det["roi_id"])
|
||||
|
||||
object_3d = det.get("object_3d")
|
||||
if object_3d is not None:
|
||||
item["xyzlhwyaw"] = [format_scalar(v) for v in object_3d[:7]]
|
||||
|
||||
object_3d_ego = det.get("object_3d_ego")
|
||||
if object_3d_ego is not None:
|
||||
item["xyzlhwyaw_ego"] = [format_scalar(v) for v in object_3d_ego[:7]]
|
||||
|
||||
if keep_all_fields:
|
||||
for key, value in det.items():
|
||||
if key in item or key in {"class_id", "confidence", "bbox", "object_3d", "object_3d_ego"}:
|
||||
continue
|
||||
item[key] = value
|
||||
|
||||
return item
|
||||
|
||||
|
||||
def frame_output_name(frame):
|
||||
"""Choose output filename stem for one frame."""
|
||||
image_name = frame.get("image_name") or ""
|
||||
stem = Path(image_name).stem if image_name else ""
|
||||
return normalize_image_name(stem) if stem else "unknown"
|
||||
|
||||
|
||||
def build_output_data(frame, keep_all_fields=False):
|
||||
"""Convert one tracking frame to per-frame output payload."""
|
||||
output_data = {}
|
||||
for idx, det in enumerate(frame.get("detections", [])):
|
||||
output_data[str(idx)] = normalize_detection(det, keep_all_fields=keep_all_fields)
|
||||
return output_data
|
||||
|
||||
|
||||
def write_one_frame(frame, output_dir, keep_all_fields=False, pretty=False):
|
||||
"""Write one output JSON file and return its path."""
|
||||
frame_name = frame_output_name(frame)
|
||||
output_data = build_output_data(frame, keep_all_fields=keep_all_fields)
|
||||
output_path = output_dir / f"{frame_name}.json"
|
||||
|
||||
dump_kwargs = {
|
||||
"ensure_ascii": False,
|
||||
}
|
||||
if pretty:
|
||||
dump_kwargs["indent"] = 2
|
||||
else:
|
||||
dump_kwargs["separators"] = (",", ":")
|
||||
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
json.dump(output_data, f, **dump_kwargs)
|
||||
return output_path
|
||||
|
||||
|
||||
def split_tracking_json(input_path, output_dir, keep_all_fields=False, num_workers=1, pretty=False):
|
||||
"""Split a tracking JSON list into one frame file per item."""
|
||||
input_path = Path(input_path)
|
||||
output_dir = Path(output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(input_path, "r", encoding="utf-8") as f:
|
||||
frames = json.load(f)
|
||||
|
||||
if not isinstance(frames, list):
|
||||
raise ValueError(f"Expected list of frames in {input_path}, got {type(frames).__name__}")
|
||||
|
||||
max_workers = max(1, int(num_workers))
|
||||
if max_workers == 1 or len(frames) <= 1:
|
||||
for frame in frames:
|
||||
write_one_frame(frame, output_dir, keep_all_fields=keep_all_fields, pretty=pretty)
|
||||
return len(frames)
|
||||
|
||||
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||
futures = [
|
||||
executor.submit(
|
||||
write_one_frame,
|
||||
frame,
|
||||
output_dir,
|
||||
keep_all_fields,
|
||||
pretty,
|
||||
)
|
||||
for frame in frames
|
||||
]
|
||||
for future in futures:
|
||||
future.result()
|
||||
|
||||
return len(frames)
|
||||
|
||||
|
||||
def default_output_dir(input_path):
|
||||
"""Return a default output directory next to the input JSON."""
|
||||
input_path = Path(input_path)
|
||||
return input_path.parent / f"{input_path.stem}_split"
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Split tracking JSON into evaluator-friendly per-frame JSON files."
|
||||
)
|
||||
parser.add_argument("--input", required=True, help="Path to roi0.json / tracking.json / merge.json")
|
||||
parser.add_argument("--output-dir", default=None,
|
||||
help="Directory to write per-frame JSON files (default: <input_stem>_split)")
|
||||
parser.add_argument("--keep-all-fields", action="store_true",
|
||||
help="Keep extra tracking fields beyond the evaluator-friendly schema")
|
||||
parser.add_argument("--num-workers", type=int, default=max(1, min(16, os.cpu_count() or 1)),
|
||||
help="Number of parallel workers for per-frame JSON writing (default: %(default)s)")
|
||||
parser.add_argument("--pretty", action="store_true",
|
||||
help="Write indented JSON for readability; default uses compact JSON for speed")
|
||||
args = parser.parse_args()
|
||||
|
||||
output_dir = Path(args.output_dir) if args.output_dir else default_output_dir(args.input)
|
||||
written = split_tracking_json(
|
||||
input_path=args.input,
|
||||
output_dir=output_dir,
|
||||
keep_all_fields=args.keep_all_fields,
|
||||
num_workers=args.num_workers,
|
||||
pretty=args.pretty,
|
||||
)
|
||||
|
||||
print(f"Input tracking JSON : {args.input}")
|
||||
print(f"Output directory : {output_dir}")
|
||||
print(f"Frames written : {written}")
|
||||
print(f"Worker count : {args.num_workers}")
|
||||
print(f"Pretty JSON : {args.pretty}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
132
tools/temporal_analysis/split_tracking_json.sh
Executable file
132
tools/temporal_analysis/split_tracking_json.sh
Executable file
@@ -0,0 +1,132 @@
|
||||
#!/bin/bash
|
||||
# 用法说明:
|
||||
# 将跟踪后的聚合 JSON(如 roi0.json)拆分为逐帧 JSON 文件,便于接入评测工具。
|
||||
#
|
||||
# 默认对 RESULTS_ROOT/MODEL_NAME 下所有 case 批量处理 roi0.json:
|
||||
# {case_dir}/roi0.json -> {case_dir}/json_results/*.json
|
||||
#
|
||||
# 支持两种模式:
|
||||
# 1) 批量模式(默认)
|
||||
# bash tools/temporal_analysis/split_tracking_json.sh
|
||||
# 2) 单 case 模式
|
||||
# bash tools/temporal_analysis/split_tracking_json.sh /path/to/case
|
||||
#
|
||||
# 可选第二个参数指定输入 JSON 文件名:
|
||||
# bash tools/temporal_analysis/split_tracking_json.sh /path/to/case roi1.json
|
||||
|
||||
set -u
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
export PYTHONPATH="${PROJECT_ROOT}:${PYTHONPATH:-}"
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 配置区
|
||||
# -----------------------------------------------------------------------
|
||||
RESULTS_ROOT="/data1/dongying/Mono3d/G1M3/cases_regular"
|
||||
MODEL_NAME="model_20260317"
|
||||
|
||||
DEFAULT_INPUT_JSON="merge.json"
|
||||
DEFAULT_OUTPUT_DIR="json_results_merge"
|
||||
KEEP_ALL_FIELDS=0
|
||||
NUM_WORKERS_PER_CASE=8
|
||||
NUM_CASE_WORKERS=8
|
||||
|
||||
split_one_case() {
|
||||
local case_dir="$1"
|
||||
local input_json_name="$2"
|
||||
local output_dir_name="$3"
|
||||
|
||||
if [[ ! -d "${case_dir}" ]]; then
|
||||
echo "Error: case directory does not exist: ${case_dir}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local input_json="${case_dir}/${input_json_name}"
|
||||
local output_dir="${case_dir}/${output_dir_name}"
|
||||
|
||||
if [[ ! -f "${input_json}" ]]; then
|
||||
echo "Warning: input tracking JSON not found, skipping: ${input_json}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- Split tracking JSON ---"
|
||||
echo "Case : ${case_dir}"
|
||||
echo "Input JSON : ${input_json}"
|
||||
echo "Output dir : ${output_dir}"
|
||||
|
||||
local cmd=(
|
||||
python3 "${PROJECT_ROOT}/tools/temporal_analysis/split_tracking_json.py"
|
||||
--input "${input_json}"
|
||||
--output-dir "${output_dir}"
|
||||
--num-workers "${NUM_WORKERS_PER_CASE}"
|
||||
)
|
||||
|
||||
if [[ "${KEEP_ALL_FIELDS}" == "1" ]]; then
|
||||
cmd+=(--keep-all-fields)
|
||||
fi
|
||||
|
||||
"${cmd[@]}"
|
||||
}
|
||||
|
||||
run_with_limited_jobs() {
|
||||
local max_jobs="$1"
|
||||
while true; do
|
||||
local current_jobs
|
||||
current_jobs=$(jobs -pr | wc -l)
|
||||
if [[ "${current_jobs}" -lt "${max_jobs}" ]]; then
|
||||
break
|
||||
fi
|
||||
sleep 0.2
|
||||
done
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "######################################################################"
|
||||
echo "# Split tracking JSON into per-frame evaluator-friendly JSON files"
|
||||
echo "######################################################################"
|
||||
|
||||
if [[ -n "${1:-}" ]]; then
|
||||
CASE_DIR="$1"
|
||||
INPUT_JSON_NAME="${2:-${DEFAULT_INPUT_JSON}}"
|
||||
echo "Single-case mode: ${CASE_DIR}"
|
||||
split_one_case "${CASE_DIR}" "${INPUT_JSON_NAME}" "${DEFAULT_OUTPUT_DIR}"
|
||||
exit $?
|
||||
fi
|
||||
|
||||
MODEL_DIR="${RESULTS_ROOT}/${MODEL_NAME}"
|
||||
if [[ ! -d "${MODEL_DIR}" ]]; then
|
||||
echo "Error: model directory not found: ${MODEL_DIR}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mapfile -t CASE_DIRS < <(find "${MODEL_DIR}" -mindepth 1 -maxdepth 1 -type d | sort)
|
||||
|
||||
if [[ ${#CASE_DIRS[@]} -eq 0 ]]; then
|
||||
echo "Warning: no case directories found under ${MODEL_DIR}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Batch mode: found ${#CASE_DIRS[@]} case(s) under ${MODEL_DIR}"
|
||||
echo "Input JSON name : ${DEFAULT_INPUT_JSON}"
|
||||
echo "Output dir name : ${DEFAULT_OUTPUT_DIR}"
|
||||
echo "Case workers : ${NUM_CASE_WORKERS}"
|
||||
echo "Frame workers : ${NUM_WORKERS_PER_CASE}"
|
||||
|
||||
declare -a PIDS=()
|
||||
for case_dir in "${CASE_DIRS[@]}"; do
|
||||
run_with_limited_jobs "${NUM_CASE_WORKERS}"
|
||||
split_one_case "${case_dir}" "${DEFAULT_INPUT_JSON}" "${DEFAULT_OUTPUT_DIR}" &
|
||||
PIDS+=("$!")
|
||||
done
|
||||
|
||||
for pid in "${PIDS[@]}"; do
|
||||
if ! wait "${pid}"; then
|
||||
echo "Error: one or more split jobs failed."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Split complete."
|
||||
562
tools/temporal_analysis/track_event_objects.py
Executable file
562
tools/temporal_analysis/track_event_objects.py
Executable file
@@ -0,0 +1,562 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Track exported inference outputs at event scope across multiple clips.
|
||||
|
||||
This tool aggregates all clip-level per-frame JSON predictions under one
|
||||
event directory, orders frames globally by timestamp parsed from filenames,
|
||||
and then reuses the existing tracking logic from track_objects.py to produce
|
||||
one tracking result set per event.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
FILE = Path(__file__).resolve()
|
||||
THIS_DIR = FILE.parent
|
||||
if str(THIS_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(THIS_DIR))
|
||||
|
||||
from merge_tracking_results import TRACK_ID_OFFSET_PER_SOURCE # noqa: E402
|
||||
from track_objects import ( # noqa: E402
|
||||
TRACKED_CLASS_IDS,
|
||||
count_unique_tracks,
|
||||
parse_det_format,
|
||||
save_tracking_results,
|
||||
track_objects,
|
||||
)
|
||||
|
||||
|
||||
SOURCE_SPECS = (
|
||||
("roi0", "roi0.json"),
|
||||
("roi1", "roi1.json"),
|
||||
("merge", "merge.json"),
|
||||
)
|
||||
DEFAULT_EVENT_OUTPUT_DIRNAME = "event_tracking"
|
||||
|
||||
|
||||
@dataclass
|
||||
class EventFrameRecord:
|
||||
key: str
|
||||
clip_case_name: str
|
||||
clip_token: str
|
||||
original_image_name: str
|
||||
original_frame_id: Optional[int]
|
||||
timestamp: Optional[int]
|
||||
source_files: dict[str, Path] = field(default_factory=dict)
|
||||
event_frame_index: int = -1
|
||||
event_frame_id: int = -1
|
||||
image_name: str = ""
|
||||
|
||||
|
||||
def _safe_int(value: Any) -> Optional[int]:
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
return int(str(value).strip())
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _normalize_output_dir_token(value: Any) -> str:
|
||||
token = re.sub(r'[\\/:*?"<>|\s]+', "_", str(value or "").strip())
|
||||
return token.strip("._")
|
||||
|
||||
|
||||
def _extract_date_name_from_records(records: Any) -> Optional[str]:
|
||||
if not isinstance(records, list):
|
||||
return None
|
||||
|
||||
for record in records:
|
||||
if not isinstance(record, dict):
|
||||
continue
|
||||
source_record = record.get("source_record")
|
||||
if not isinstance(source_record, dict):
|
||||
continue
|
||||
for key in ("date_name", "datename", "datetime", "date"):
|
||||
value = source_record.get(key)
|
||||
normalized = _normalize_output_dir_token(value)
|
||||
if normalized:
|
||||
return normalized
|
||||
return None
|
||||
|
||||
|
||||
def parse_frame_name_metadata(image_name: str) -> tuple[str, Optional[int], Optional[int]]:
|
||||
"""Parse clip token, frame_id, and timestamp from an exported frame stem."""
|
||||
stem = str(image_name or "").strip()
|
||||
if not stem:
|
||||
return "", None, None
|
||||
|
||||
parts = stem.split("_")
|
||||
numeric_tail = []
|
||||
while parts and parts[-1].isdigit() and len(numeric_tail) < 2:
|
||||
numeric_tail.append(parts.pop())
|
||||
|
||||
numeric_tail.reverse()
|
||||
clip_token = "_".join(parts).strip("_")
|
||||
if len(numeric_tail) >= 2:
|
||||
return clip_token, _safe_int(numeric_tail[0]), _safe_int(numeric_tail[1])
|
||||
if len(numeric_tail) == 1:
|
||||
return clip_token, _safe_int(numeric_tail[0]), None
|
||||
return stem, None, None
|
||||
|
||||
|
||||
def build_event_sort_key(record: EventFrameRecord) -> tuple[Any, ...]:
|
||||
timestamp_missing = record.timestamp is None
|
||||
timestamp_value = record.timestamp if record.timestamp is not None else float("inf")
|
||||
frame_id_missing = record.original_frame_id is None
|
||||
frame_id_value = record.original_frame_id if record.original_frame_id is not None else float("inf")
|
||||
return (
|
||||
timestamp_missing,
|
||||
timestamp_value,
|
||||
record.clip_case_name,
|
||||
frame_id_missing,
|
||||
frame_id_value,
|
||||
record.original_image_name,
|
||||
)
|
||||
|
||||
|
||||
def find_event_case_dirs(event_dir: Path) -> list[Path]:
|
||||
"""Return all clip-case directories directly under one event directory."""
|
||||
case_dirs = []
|
||||
for predictions_dir in sorted(event_dir.glob("*/predictions")):
|
||||
if not predictions_dir.is_dir():
|
||||
continue
|
||||
case_dirs.append(predictions_dir.parent)
|
||||
return case_dirs
|
||||
|
||||
|
||||
def collect_event_frames(
|
||||
event_dir: Path,
|
||||
pattern: str,
|
||||
*,
|
||||
verbose: bool = True,
|
||||
) -> tuple[list[EventFrameRecord], list[Path]]:
|
||||
"""Collect and globally order all per-frame JSON files under one event."""
|
||||
case_dirs = find_event_case_dirs(event_dir)
|
||||
if not case_dirs:
|
||||
raise FileNotFoundError(f"No clip case predictions found under event directory: {event_dir}")
|
||||
|
||||
frame_map: dict[str, EventFrameRecord] = {}
|
||||
source_file_count = 0
|
||||
|
||||
for case_dir in case_dirs:
|
||||
predictions_dir = case_dir / "predictions"
|
||||
for source_name, _ in SOURCE_SPECS:
|
||||
source_dir = predictions_dir / source_name
|
||||
if not source_dir.is_dir():
|
||||
continue
|
||||
|
||||
for json_file in sorted(source_dir.glob(pattern)):
|
||||
source_file_count += 1
|
||||
original_image_name = json_file.stem
|
||||
clip_token, frame_id, timestamp = parse_frame_name_metadata(original_image_name)
|
||||
key = f"{case_dir.name}:{original_image_name}"
|
||||
record = frame_map.get(key)
|
||||
if record is None:
|
||||
record = EventFrameRecord(
|
||||
key=key,
|
||||
clip_case_name=case_dir.name,
|
||||
clip_token=clip_token,
|
||||
original_image_name=original_image_name,
|
||||
original_frame_id=frame_id,
|
||||
timestamp=timestamp,
|
||||
)
|
||||
frame_map[key] = record
|
||||
elif record.timestamp is None and timestamp is not None:
|
||||
record.timestamp = timestamp
|
||||
elif record.original_frame_id is None and frame_id is not None:
|
||||
record.original_frame_id = frame_id
|
||||
|
||||
record.source_files[source_name] = json_file
|
||||
|
||||
ordered_frames = sorted(frame_map.values(), key=build_event_sort_key)
|
||||
if not ordered_frames:
|
||||
raise FileNotFoundError(
|
||||
f"No source frame JSON files matching {pattern!r} were found under event directory: {event_dir}"
|
||||
)
|
||||
|
||||
for frame_index, record in enumerate(ordered_frames):
|
||||
event_frame_id = frame_index + 1
|
||||
timestamp_token = record.timestamp if record.timestamp is not None else event_frame_id
|
||||
record.event_frame_index = frame_index
|
||||
record.event_frame_id = event_frame_id
|
||||
record.image_name = f"camera4_{event_frame_id:06d}_{int(timestamp_token)}"
|
||||
|
||||
if verbose:
|
||||
print(f"Discovered {len(case_dirs)} clip case(s) under {event_dir}")
|
||||
print(f"Collected {source_file_count} source frame JSON file(s)")
|
||||
print(f"Built {len(ordered_frames)} event frame(s) after cross-clip ordering")
|
||||
|
||||
return ordered_frames, case_dirs
|
||||
|
||||
|
||||
def load_event_metadata(event_dir: Path) -> dict[str, Any]:
|
||||
"""Load optional event manifest metadata for reporting only."""
|
||||
manifest_path = event_dir / "_status" / "event_manifest.json"
|
||||
payload: dict[str, Any] = {}
|
||||
if manifest_path.is_file():
|
||||
with manifest_path.open("r", encoding="utf-8") as file:
|
||||
payload = json.load(file)
|
||||
|
||||
event_id = payload.get("event_id", event_dir.name)
|
||||
scene = payload.get("scene", event_dir.parent.name)
|
||||
date_name = _extract_date_name_from_records(payload.get("records"))
|
||||
|
||||
if not date_name:
|
||||
scene_manifest_path = event_dir.parent / "_status" / "scene_event_manifest.json"
|
||||
if scene_manifest_path.is_file():
|
||||
with scene_manifest_path.open("r", encoding="utf-8") as file:
|
||||
scene_payload = json.load(file)
|
||||
scene_records = scene_payload.get("records", [])
|
||||
matched_records = [
|
||||
record for record in scene_records
|
||||
if isinstance(record, dict) and str(record.get("event_id", "")).strip() == str(event_id)
|
||||
]
|
||||
date_name = _extract_date_name_from_records(matched_records)
|
||||
|
||||
if not date_name:
|
||||
date_name = DEFAULT_EVENT_OUTPUT_DIRNAME
|
||||
|
||||
return {
|
||||
"event_id": event_id,
|
||||
"scene": scene,
|
||||
"manifest_path": str(manifest_path) if manifest_path.is_file() else "",
|
||||
"clip_ids": payload.get("clip_ids", []),
|
||||
"clip_count": int(payload.get("clip_count", 0) or 0),
|
||||
"date_name": date_name,
|
||||
}
|
||||
|
||||
|
||||
def build_frame_info(record: EventFrameRecord, source_name: str) -> dict[str, Any]:
|
||||
"""Build frame metadata that will be copied through track_objects.py."""
|
||||
return {
|
||||
"event_frame_index": record.event_frame_index,
|
||||
"event_frame_id": record.event_frame_id,
|
||||
"source": source_name,
|
||||
"clip_case_name": record.clip_case_name,
|
||||
"clip_token": record.clip_token,
|
||||
"original_image_name": record.original_image_name,
|
||||
"original_frame_id": record.original_frame_id,
|
||||
"timestamp": record.timestamp,
|
||||
"source_json_path": str(record.source_files[source_name]),
|
||||
}
|
||||
|
||||
|
||||
def load_source_predictions(
|
||||
ordered_frames: list[EventFrameRecord],
|
||||
source_name: str,
|
||||
*,
|
||||
model_version: Optional[str] = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Load all available frames for one source in event-global temporal order."""
|
||||
predictions_data: list[dict[str, Any]] = []
|
||||
for record in ordered_frames:
|
||||
source_file = record.source_files.get(source_name)
|
||||
if source_file is None:
|
||||
continue
|
||||
|
||||
with source_file.open("r", encoding="utf-8") as file:
|
||||
det_dict = json.load(file)
|
||||
frame_info = build_frame_info(record, source_name)
|
||||
frame_data = parse_det_format(
|
||||
det_dict,
|
||||
image_name=record.image_name,
|
||||
timestamp_lookup=None,
|
||||
model_version=model_version,
|
||||
frame_info=frame_info,
|
||||
)
|
||||
frame_data["frame_info"] = frame_info
|
||||
predictions_data.append(frame_data)
|
||||
return predictions_data
|
||||
|
||||
|
||||
def merge_event_tracking_results(
|
||||
*,
|
||||
ordered_frames: list[EventFrameRecord],
|
||||
tracking_results_by_source: dict[str, list[dict[str, Any]]],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Merge per-source event tracking results while preserving event order."""
|
||||
frame_maps = {
|
||||
source_name: {
|
||||
frame.get("image_name"): frame
|
||||
for frame in tracking_results
|
||||
}
|
||||
for source_name, tracking_results in tracking_results_by_source.items()
|
||||
}
|
||||
|
||||
merged_frames: list[dict[str, Any]] = []
|
||||
for ordered_frame in ordered_frames:
|
||||
image_name = ordered_frame.image_name
|
||||
merged_detections = []
|
||||
merged_stats = {}
|
||||
frame_info = None
|
||||
|
||||
for source_idx, (source_name, _) in enumerate(SOURCE_SPECS):
|
||||
frame = frame_maps.get(source_name, {}).get(image_name)
|
||||
if frame is None:
|
||||
continue
|
||||
|
||||
frame_info = frame_info or frame.get("frame_info")
|
||||
for det in frame.get("detections", []):
|
||||
tagged = dict(det)
|
||||
tagged["lane_assignment"] = source_idx
|
||||
if "track_id" in tagged and tagged["track_id"] is not None:
|
||||
tagged["track_id"] = tagged["track_id"] + source_idx * TRACK_ID_OFFSET_PER_SOURCE
|
||||
merged_detections.append(tagged)
|
||||
|
||||
if "tracking_stats" in frame:
|
||||
merged_stats[source_name] = frame["tracking_stats"]
|
||||
|
||||
if not merged_detections and not merged_stats:
|
||||
continue
|
||||
|
||||
merged_frame = {
|
||||
"image_name": image_name,
|
||||
"detections": merged_detections,
|
||||
}
|
||||
if frame_info is not None:
|
||||
merged_frame["frame_info"] = frame_info
|
||||
if merged_stats:
|
||||
merged_frame["tracking_stats"] = merged_stats
|
||||
merged_frames.append(merged_frame)
|
||||
|
||||
return merged_frames
|
||||
|
||||
|
||||
def build_frame_manifest_payload(
|
||||
*,
|
||||
event_dir: Path,
|
||||
output_dir: Path,
|
||||
event_metadata: dict[str, Any],
|
||||
case_dirs: list[Path],
|
||||
ordered_frames: list[EventFrameRecord],
|
||||
source_summaries: dict[str, dict[str, Any]],
|
||||
merge_output_path: Path,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"event_dir": str(event_dir),
|
||||
"output_dir": str(output_dir),
|
||||
"event_id": event_metadata.get("event_id", event_dir.name),
|
||||
"scene": event_metadata.get("scene", event_dir.parent.name),
|
||||
"date_name": event_metadata.get("date_name", DEFAULT_EVENT_OUTPUT_DIRNAME),
|
||||
"event_manifest_path": event_metadata.get("manifest_path", ""),
|
||||
"clip_ids": event_metadata.get("clip_ids", []),
|
||||
"clip_count": event_metadata.get("clip_count", len(case_dirs)),
|
||||
"clip_case_dirs": [str(case_dir) for case_dir in case_dirs],
|
||||
"source_summaries": source_summaries,
|
||||
"merge_output_path": str(merge_output_path),
|
||||
"event_frame_count": len(ordered_frames),
|
||||
"frames": [
|
||||
{
|
||||
"event_frame_index": record.event_frame_index,
|
||||
"event_frame_id": record.event_frame_id,
|
||||
"image_name": record.image_name,
|
||||
"timestamp": record.timestamp,
|
||||
"clip_case_name": record.clip_case_name,
|
||||
"clip_token": record.clip_token,
|
||||
"original_image_name": record.original_image_name,
|
||||
"original_frame_id": record.original_frame_id,
|
||||
"source_files": {
|
||||
source_name: str(path)
|
||||
for source_name, path in sorted(record.source_files.items())
|
||||
},
|
||||
}
|
||||
for record in ordered_frames
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def run_event_tracking(
|
||||
*,
|
||||
event_dir: Path,
|
||||
output_dir: Path,
|
||||
file_pattern: str,
|
||||
classes: list[int],
|
||||
iou_threshold: float,
|
||||
max_age: int,
|
||||
min_hits: int,
|
||||
distance_threshold: float,
|
||||
use_3d: bool,
|
||||
max_3d_distance: float,
|
||||
model_version: Optional[str],
|
||||
merge_output_name: str,
|
||||
manifest_name: str,
|
||||
verbose: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
event_metadata = load_event_metadata(event_dir)
|
||||
ordered_frames, case_dirs = collect_event_frames(event_dir, file_pattern, verbose=verbose)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
tracking_results_by_source: dict[str, list[dict[str, Any]]] = {}
|
||||
source_summaries: dict[str, dict[str, Any]] = {}
|
||||
|
||||
for source_name, output_name in SOURCE_SPECS:
|
||||
predictions_data = load_source_predictions(
|
||||
ordered_frames,
|
||||
source_name,
|
||||
model_version=model_version,
|
||||
)
|
||||
output_path = output_dir / output_name
|
||||
|
||||
if not predictions_data:
|
||||
source_summaries[source_name] = {
|
||||
"ok": False,
|
||||
"reason": "no_frames",
|
||||
"frames": 0,
|
||||
"unique_tracks": 0,
|
||||
"output_path": str(output_path),
|
||||
}
|
||||
if verbose:
|
||||
print(f"Warning: no frames found for source {source_name} under {event_dir}")
|
||||
continue
|
||||
|
||||
if verbose:
|
||||
print("")
|
||||
print(f"--- Tracking {source_name} at event scope ---")
|
||||
print(f"Frames: {len(predictions_data)}")
|
||||
print(f"Output: {output_path}")
|
||||
|
||||
tracking_results = track_objects(
|
||||
predictions_data,
|
||||
target_classes=classes,
|
||||
iou_threshold=iou_threshold,
|
||||
max_age=max_age,
|
||||
min_hits=min_hits,
|
||||
distance_threshold=distance_threshold,
|
||||
use_3d=use_3d,
|
||||
max_3d_distance=max_3d_distance,
|
||||
verbose=verbose,
|
||||
)
|
||||
save_tracking_results(tracking_results, output_path)
|
||||
tracking_results_by_source[source_name] = tracking_results
|
||||
source_summaries[source_name] = {
|
||||
"ok": True,
|
||||
"frames": len(predictions_data),
|
||||
"unique_tracks": count_unique_tracks(tracking_results),
|
||||
"output_path": str(output_path),
|
||||
}
|
||||
|
||||
combined_output_path = output_dir / merge_output_name
|
||||
if not tracking_results_by_source:
|
||||
raise RuntimeError(f"No valid source predictions were loaded for event: {event_dir}")
|
||||
|
||||
combined_tracking = merge_event_tracking_results(
|
||||
ordered_frames=ordered_frames,
|
||||
tracking_results_by_source=tracking_results_by_source,
|
||||
)
|
||||
save_tracking_results(combined_tracking, combined_output_path)
|
||||
|
||||
manifest_path = output_dir / manifest_name
|
||||
manifest_payload = build_frame_manifest_payload(
|
||||
event_dir=event_dir,
|
||||
output_dir=output_dir,
|
||||
event_metadata=event_metadata,
|
||||
case_dirs=case_dirs,
|
||||
ordered_frames=ordered_frames,
|
||||
source_summaries=source_summaries,
|
||||
merge_output_path=combined_output_path,
|
||||
)
|
||||
with manifest_path.open("w", encoding="utf-8") as file:
|
||||
json.dump(manifest_payload, file, indent=2, ensure_ascii=False)
|
||||
|
||||
if verbose:
|
||||
print("")
|
||||
print("==========================================")
|
||||
print(f"Event : {event_metadata.get('event_id', event_dir.name)}")
|
||||
print(f"Scene : {event_metadata.get('scene', event_dir.parent.name)}")
|
||||
print(f"Date : {event_metadata.get('date_name', DEFAULT_EVENT_OUTPUT_DIRNAME)}")
|
||||
print(f"Clips : {len(case_dirs)}")
|
||||
print(f"Frames : {len(ordered_frames)}")
|
||||
print(f"Merge : {combined_output_path}")
|
||||
print(f"Manifest: {manifest_path}")
|
||||
for source_name, _ in SOURCE_SPECS:
|
||||
summary = source_summaries.get(source_name, {})
|
||||
status = "ok" if summary.get("ok") else summary.get("reason", "skipped")
|
||||
print(
|
||||
f" - {source_name}: {status}, frames={summary.get('frames', 0)}, "
|
||||
f"tracks={summary.get('unique_tracks', 0)}"
|
||||
)
|
||||
print("==========================================")
|
||||
|
||||
return {
|
||||
"event_dir": str(event_dir),
|
||||
"output_dir": str(output_dir),
|
||||
"manifest_path": str(manifest_path),
|
||||
"merge_output_path": str(combined_output_path),
|
||||
"event_frame_count": len(ordered_frames),
|
||||
"clip_case_count": len(case_dirs),
|
||||
"source_summaries": source_summaries,
|
||||
}
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Track all clip-level exported inference results under one event directory."
|
||||
)
|
||||
parser.add_argument("--event-dir", required=True, help="Event directory containing multiple clip-case outputs")
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
default=None,
|
||||
help="Output directory for event-level tracking results (default: <event-dir>/event_tracking)",
|
||||
)
|
||||
parser.add_argument("--file-pattern", default="*.json", help="Glob pattern for per-frame JSONs in each source dir")
|
||||
parser.add_argument(
|
||||
"--classes",
|
||||
type=int,
|
||||
nargs="+",
|
||||
default=None,
|
||||
help="Class IDs to track (default: track_objects.py defaults)",
|
||||
)
|
||||
parser.add_argument("--iou-threshold", type=float, default=0.3)
|
||||
parser.add_argument("--max-age", type=int, default=5)
|
||||
parser.add_argument("--min-hits", type=int, default=1)
|
||||
parser.add_argument("--distance-threshold", type=float, default=100.0)
|
||||
parser.add_argument("--model-version", type=str, default=None)
|
||||
parser.add_argument("--use-3d", action="store_true")
|
||||
parser.add_argument("--max-3d-distance", type=float, default=10.0)
|
||||
parser.add_argument("--merge-output-name", type=str, default="combined_tracking.json")
|
||||
parser.add_argument("--manifest-name", type=str, default="frame_order_manifest.json")
|
||||
parser.add_argument("--quiet", action="store_true", help="Reduce progress logging")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
event_dir = Path(args.event_dir).resolve()
|
||||
if not event_dir.is_dir():
|
||||
raise FileNotFoundError(f"Event directory does not exist: {event_dir}")
|
||||
|
||||
output_dir = (
|
||||
Path(args.output_dir).resolve()
|
||||
if args.output_dir is not None
|
||||
else event_dir / load_event_metadata(event_dir).get("date_name", DEFAULT_EVENT_OUTPUT_DIRNAME)
|
||||
)
|
||||
classes = list(TRACKED_CLASS_IDS) if args.classes is None else [int(cls_id) for cls_id in args.classes]
|
||||
|
||||
run_event_tracking(
|
||||
event_dir=event_dir,
|
||||
output_dir=output_dir,
|
||||
file_pattern=args.file_pattern,
|
||||
classes=classes,
|
||||
iou_threshold=args.iou_threshold,
|
||||
max_age=args.max_age,
|
||||
min_hits=args.min_hits,
|
||||
distance_threshold=args.distance_threshold,
|
||||
use_3d=args.use_3d,
|
||||
max_3d_distance=args.max_3d_distance,
|
||||
model_version=args.model_version,
|
||||
merge_output_name=args.merge_output_name,
|
||||
manifest_name=args.manifest_name,
|
||||
verbose=not args.quiet,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1708
tools/temporal_analysis/track_objects.py
Executable file
1708
tools/temporal_analysis/track_objects.py
Executable file
File diff suppressed because it is too large
Load Diff
134
tools/temporal_analysis/track_objects.sh
Executable file
134
tools/temporal_analysis/track_objects.sh
Executable file
@@ -0,0 +1,134 @@
|
||||
#!/bin/bash
|
||||
# 用法说明:
|
||||
# 批量对 test_merged_model.sh 推理结果进行跟踪后处理,覆盖三路输入:
|
||||
# ROI0: {case_dir}/roi0/ → {case_dir}/roi0.json
|
||||
# ROI1: {case_dir}/roi1/ → {case_dir}/roi1.json
|
||||
# Merge: {case_dir}/merge_json/ → {case_dir}/merge.json
|
||||
# 并将三路跟踪结果合并为:
|
||||
# {case_dir}/combined_tracking.json
|
||||
#
|
||||
# 支持两种模式:
|
||||
# 1) 批量模式(默认):处理 RESULTS_ROOT/MODEL_NAME 下所有 case
|
||||
# bash track_objects.sh
|
||||
# 2) 单 case 模式:指定 case 目录路径作为第一个参数
|
||||
# bash track_objects.sh /data1/dongying/Mono3d/G1M3/cases_regular/model_20260228/some_case
|
||||
|
||||
# Set PYTHONPATH to project root for module imports
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
export PYTHONPATH="${PROJECT_ROOT}:${PYTHONPATH}"
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 配置区:与 test_merged_model.sh 保持一致
|
||||
# -----------------------------------------------------------------------
|
||||
# RESULTS_ROOT="/data1/dongying/Mono3d/G1M3/cases_feishu_results"
|
||||
# MODEL_NAME="torchscript_python_300w_newdata-cncap-v6-ego"
|
||||
# /data1/dongying/Mono3d/G1Q3/tmp/20251018215336_results/predictions
|
||||
|
||||
RESULTS_ROOT="/data1/dongying/Mono3d/G1M3/cases_regular"
|
||||
RESULTS_ROOT="/data1/dongying/Mono3d/G1M3/pdcl_batch_infer/cases_aeb_v2/CBNA/"
|
||||
MODEL_NAME="50_15_60"
|
||||
NUM_WORKERS=16
|
||||
|
||||
# 公共跟踪参数
|
||||
TRACK_CLASSES="${TRACK_CLASSES:-0 1 2 3 4 5 6 7 8 9 10 11 12}"
|
||||
IOU_THRESH=0.3
|
||||
MAX_AGE=5
|
||||
DIST_THRESH=100
|
||||
MAX_FRAMES="${MAX_FRAMES:-200}"
|
||||
MODEL_VERSION="${MODEL_VERSION:-model_20260403}"
|
||||
|
||||
echo ""
|
||||
echo "######################################################################"
|
||||
echo "# Batch tracking and merge: single scan + case-level parallel execution"
|
||||
echo "######################################################################"
|
||||
|
||||
FRAME_LIMIT_ARGS=()
|
||||
if [[ -n "${MAX_FRAMES}" ]]; then
|
||||
FRAME_LIMIT_ARGS+=(--max-frames "${MAX_FRAMES}")
|
||||
fi
|
||||
|
||||
MODEL_VERSION_ARGS=()
|
||||
if [[ -n "${MODEL_VERSION}" ]]; then
|
||||
MODEL_VERSION_ARGS+=(--model-version "${MODEL_VERSION}")
|
||||
fi
|
||||
|
||||
if [[ -n "$1" ]]; then
|
||||
# ===================================================================
|
||||
# 单 case 模式:对指定 case 目录进行跟踪 + 合并
|
||||
# ===================================================================
|
||||
CASE_DIR="$1"
|
||||
if [[ ! -d "${CASE_DIR}" ]]; then
|
||||
echo "Error: case directory does not exist: ${CASE_DIR}"
|
||||
exit 1
|
||||
fi
|
||||
echo "Single-case mode: ${CASE_DIR}"
|
||||
|
||||
# 逐源跟踪
|
||||
for SOURCE in roi0 roi1 merge; do
|
||||
INPUT_DIR="${CASE_DIR}/${SOURCE}"
|
||||
if [[ "${SOURCE}" == "merge" ]]; then
|
||||
OUTPUT_FILE="${CASE_DIR}/merge.json"
|
||||
else
|
||||
OUTPUT_FILE="${CASE_DIR}/${SOURCE}.json"
|
||||
fi
|
||||
if [[ -d "${INPUT_DIR}" ]]; then
|
||||
echo ""
|
||||
echo "--- Tracking ${SOURCE} ---"
|
||||
python "${PROJECT_ROOT}/tools/temporal_analysis/track_objects.py" \
|
||||
--input-dir "${INPUT_DIR}" \
|
||||
--output "${OUTPUT_FILE}" \
|
||||
--file-pattern "*.json" \
|
||||
--classes ${TRACK_CLASSES} \
|
||||
--iou-threshold ${IOU_THRESH} \
|
||||
--max-age ${MAX_AGE} \
|
||||
--distance-threshold ${DIST_THRESH} \
|
||||
"${FRAME_LIMIT_ARGS[@]}" \
|
||||
"${MODEL_VERSION_ARGS[@]}"
|
||||
else
|
||||
echo "Warning: ${INPUT_DIR} not found, skipping ${SOURCE}"
|
||||
fi
|
||||
done
|
||||
|
||||
# 合并三路跟踪结果
|
||||
echo ""
|
||||
echo "--- Merging tracking results ---"
|
||||
python "${PROJECT_ROOT}/tools/temporal_analysis/merge_tracking_results.py" \
|
||||
--roi0 "${CASE_DIR}/roi0.json" \
|
||||
--roi1 "${CASE_DIR}/roi1.json" \
|
||||
--merge "${CASE_DIR}/merge.json" \
|
||||
--output "${CASE_DIR}/combined_tracking.json"
|
||||
|
||||
else
|
||||
# ===================================================================
|
||||
# 批量模式:扫描 RESULTS_ROOT/MODEL_NAME 下所有 case
|
||||
# ===================================================================
|
||||
python "${PROJECT_ROOT}/tools/temporal_analysis/track_objects.py" \
|
||||
--results-root "${RESULTS_ROOT}" \
|
||||
--model-name "${MODEL_NAME}" \
|
||||
--num-workers "${NUM_WORKERS}" \
|
||||
--file-pattern "*.json" \
|
||||
--classes ${TRACK_CLASSES} \
|
||||
--iou-threshold ${IOU_THRESH} \
|
||||
--max-age ${MAX_AGE} \
|
||||
--distance-threshold ${DIST_THRESH} \
|
||||
"${FRAME_LIMIT_ARGS[@]}" \
|
||||
"${MODEL_VERSION_ARGS[@]}"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 附加3D距离增强匹配(注释状态,按需启用)
|
||||
# -----------------------------------------------------------------------
|
||||
# while IFS= read -r -d '' merge_json_dir; do
|
||||
# case_dir=$(dirname "$merge_json_dir")
|
||||
# output_file="${case_dir}/merge_tracking_3d.json"
|
||||
# python "${PROJECT_ROOT}/tools/temporal_analysis/track_objects.py" \
|
||||
# --input-dir "$merge_json_dir" \
|
||||
# --output "$output_file" \
|
||||
# --classes 0 1 2 3 4 5 6 7 8 9 10 11 12 \
|
||||
# --iou-threshold 0.3 \
|
||||
# --max-age 5 \
|
||||
# --distance-threshold 100 \
|
||||
# --use-3d \
|
||||
# --max-3d-distance 2.0
|
||||
# done < <(find "${RESULTS_ROOT}/${MODEL_NAME}" -type d -name "merge_json" -print0 | sort -z)
|
||||
215
tools/temporal_analysis/track_objects_cncap.sh
Executable file
215
tools/temporal_analysis/track_objects_cncap.sh
Executable file
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
shopt -s nullglob
|
||||
|
||||
# 用法说明:
|
||||
# 批量对 test_merged_model.sh 推理结果进行跟踪后处理,覆盖三路输入:
|
||||
# ROI0: {case_dir}/roi0/ → {case_dir}/roi0.json
|
||||
# ROI1: {case_dir}/roi1/ → {case_dir}/roi1.json
|
||||
# Merge: {case_dir}/merge_json/ → {case_dir}/merge.json
|
||||
# 并将三路跟踪结果合并为:
|
||||
# {case_dir}/combined_tracking.json
|
||||
#
|
||||
# 支持两种模式:
|
||||
# 1) 批量模式(默认):遍历 BATCH_ROOT 下所有 场景/模型 目录
|
||||
# bash track_objects_cncap.sh
|
||||
# 2) 批量模式(指定根目录):第一个参数传场景根目录
|
||||
# bash track_objects_cncap.sh /data1/dongying/Mono3d/G1M3/pdcl_batch_infer/cases_aeb_v2
|
||||
# 3) 单 case 模式:第一个参数传具体 case 目录路径
|
||||
# bash track_objects_cncap.sh /path/to/one_case
|
||||
|
||||
# Set PYTHONPATH to project root for module imports
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
export PYTHONPATH="${PROJECT_ROOT}:${PYTHONPATH:-}"
|
||||
PYTHON_BIN="${PYTHON_BIN:-/deeplearning_team/ydong/dongying/miniconda/envs/yolov5/bin/python}"
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 配置区
|
||||
# -----------------------------------------------------------------------
|
||||
# 批量根目录,目录结构约定为:
|
||||
# BATCH_ROOT/<scene_name>/<model_name>/<case_name>/{roi0,roi1,merge_json}
|
||||
BATCH_ROOT="${BATCH_ROOT:-/data1/dongying/Mono3d/G1M3/pdcl_batch_infer/cases_aeb_v2}"
|
||||
|
||||
# 可选过滤,留空表示不过滤:
|
||||
# SCENE_FILTER="CBNA"
|
||||
# MODEL_FILTER="50_15_60"
|
||||
SCENE_FILTER="${SCENE_FILTER:-}"
|
||||
MODEL_FILTER="${MODEL_FILTER:-}"
|
||||
NUM_WORKERS="${NUM_WORKERS:-16}"
|
||||
|
||||
# 公共跟踪参数
|
||||
TRACK_CLASSES="${TRACK_CLASSES:-0 1 2 3 4 5 6 7 8 9 10 11 12}"
|
||||
IOU_THRESH=0.3
|
||||
MAX_AGE=5
|
||||
DIST_THRESH=100
|
||||
MODEL_VERSION="${MODEL_VERSION:-}"
|
||||
MERGE_OUTPUT_NAME="${MERGE_OUTPUT_NAME:-combined_tracking.json}"
|
||||
|
||||
echo ""
|
||||
echo "######################################################################"
|
||||
echo "# Batch tracking and merge: auto scene/model traversal"
|
||||
echo "######################################################################"
|
||||
|
||||
MODEL_VERSION_ARGS=()
|
||||
if [[ -n "${MODEL_VERSION}" ]]; then
|
||||
MODEL_VERSION_ARGS+=(--model-version "${MODEL_VERSION}")
|
||||
fi
|
||||
|
||||
is_case_dir() {
|
||||
local dir_path="$1"
|
||||
[[ -d "${dir_path}/roi0" || -d "${dir_path}/roi1" || -d "${dir_path}/merge_json" ]]
|
||||
}
|
||||
|
||||
run_single_case() {
|
||||
local case_dir="$1"
|
||||
|
||||
echo "Single-case mode: ${case_dir}"
|
||||
|
||||
for source in roi0 roi1 merge; do
|
||||
local input_dir="${case_dir}/${source}"
|
||||
local output_file
|
||||
if [[ "${source}" == "merge" ]]; then
|
||||
output_file="${case_dir}/merge.json"
|
||||
else
|
||||
output_file="${case_dir}/${source}.json"
|
||||
fi
|
||||
|
||||
if [[ -d "${input_dir}" ]]; then
|
||||
echo ""
|
||||
echo "--- Tracking ${source} ---"
|
||||
"${PYTHON_BIN}" "${PROJECT_ROOT}/tools/temporal_analysis/track_objects.py" \
|
||||
--input-dir "${input_dir}" \
|
||||
--output "${output_file}" \
|
||||
--file-pattern "*.json" \
|
||||
--classes ${TRACK_CLASSES} \
|
||||
--iou-threshold ${IOU_THRESH} \
|
||||
--max-age ${MAX_AGE} \
|
||||
--distance-threshold ${DIST_THRESH} \
|
||||
"${MODEL_VERSION_ARGS[@]}"
|
||||
else
|
||||
echo "Warning: ${input_dir} not found, skipping ${source}"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "--- Merging tracking results ---"
|
||||
"${PYTHON_BIN}" "${PROJECT_ROOT}/tools/temporal_analysis/merge_tracking_results.py" \
|
||||
--roi0 "${case_dir}/roi0.json" \
|
||||
--roi1 "${case_dir}/roi1.json" \
|
||||
--merge "${case_dir}/merge.json" \
|
||||
--output "${case_dir}/${MERGE_OUTPUT_NAME}"
|
||||
}
|
||||
|
||||
run_one_model() {
|
||||
local results_root="$1"
|
||||
local model_name="$2"
|
||||
|
||||
echo ""
|
||||
printf '[RUN] scene=%s model=%s\n' "$(basename "${results_root}")" "${model_name}"
|
||||
|
||||
"${PYTHON_BIN}" "${PROJECT_ROOT}/tools/temporal_analysis/track_objects.py" \
|
||||
--results-root "${results_root}" \
|
||||
--model-name "${model_name}" \
|
||||
--num-workers "${NUM_WORKERS}" \
|
||||
--merge-output-name "${MERGE_OUTPUT_NAME}" \
|
||||
--file-pattern "*.json" \
|
||||
--classes ${TRACK_CLASSES} \
|
||||
--iou-threshold ${IOU_THRESH} \
|
||||
--max-age ${MAX_AGE} \
|
||||
--distance-threshold ${DIST_THRESH} \
|
||||
"${MODEL_VERSION_ARGS[@]}"
|
||||
}
|
||||
|
||||
run_batch_root() {
|
||||
local batch_root="$1"
|
||||
local total_models=0
|
||||
local success_models=0
|
||||
local failed_models=0
|
||||
local matched_scenes=0
|
||||
|
||||
echo "Batch-root mode: ${batch_root}"
|
||||
[[ -n "${SCENE_FILTER}" ]] && echo "Scene filter: ${SCENE_FILTER}"
|
||||
[[ -n "${MODEL_FILTER}" ]] && echo "Model filter: ${MODEL_FILTER}"
|
||||
|
||||
for scene_dir in "${batch_root}"/*; do
|
||||
[[ -d "${scene_dir}" ]] || continue
|
||||
local scene_name
|
||||
scene_name="$(basename "${scene_dir}")"
|
||||
|
||||
if [[ -n "${SCENE_FILTER}" ]] && [[ ! "${scene_name}" =~ ${SCENE_FILTER} ]]; then
|
||||
continue
|
||||
fi
|
||||
((matched_scenes+=1))
|
||||
|
||||
local found_model=0
|
||||
for model_dir in "${scene_dir}"/*; do
|
||||
[[ -d "${model_dir}" ]] || continue
|
||||
local model_name
|
||||
model_name="$(basename "${model_dir}")"
|
||||
|
||||
if [[ -n "${MODEL_FILTER}" ]] && [[ ! "${model_name}" =~ ${MODEL_FILTER} ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
found_model=1
|
||||
((total_models+=1))
|
||||
if run_one_model "${scene_dir}" "${model_name}"; then
|
||||
((success_models+=1))
|
||||
else
|
||||
((failed_models+=1))
|
||||
printf '[FAIL] scene=%s model=%s\n' "${scene_name}" "${model_name}" >&2
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "${found_model}" == "0" ]]; then
|
||||
printf '[SKIP] empty scene dir or no matched model: %s\n' "${scene_name}"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "${matched_scenes}" -eq 0 ]]; then
|
||||
echo "[ERROR] No matched scene directories found under: ${batch_root}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ "${total_models}" -eq 0 ]]; then
|
||||
echo "[ERROR] No matched model directories found under: ${batch_root}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
printf '[DONE] models=%d success=%d failed=%d\n' \
|
||||
"${total_models}" "${success_models}" "${failed_models}"
|
||||
|
||||
[[ "${failed_models}" -eq 0 ]]
|
||||
}
|
||||
|
||||
TARGET_PATH="${1:-${BATCH_ROOT}}"
|
||||
|
||||
if [[ ! -d "${TARGET_PATH}" ]]; then
|
||||
echo "Error: target directory does not exist: ${TARGET_PATH}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if is_case_dir "${TARGET_PATH}"; then
|
||||
run_single_case "${TARGET_PATH}"
|
||||
else
|
||||
run_batch_root "${TARGET_PATH}"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 附加3D距离增强匹配(注释状态,按需启用)
|
||||
# -----------------------------------------------------------------------
|
||||
# while IFS= read -r -d '' merge_json_dir; do
|
||||
# case_dir=$(dirname "$merge_json_dir")
|
||||
# output_file="${case_dir}/merge_tracking_3d.json"
|
||||
# "${PYTHON_BIN}" "${PROJECT_ROOT}/tools/temporal_analysis/track_objects.py" \
|
||||
# --input-dir "$merge_json_dir" \
|
||||
# --output "$output_file" \
|
||||
# --classes 0 1 2 3 4 5 6 7 8 9 10 11 12 \
|
||||
# --iou-threshold 0.3 \
|
||||
# --max-age 5 \
|
||||
# --distance-threshold 100 \
|
||||
# --use-3d \
|
||||
# --max-3d-distance 2.0
|
||||
# done < <(find "${BATCH_ROOT}" -mindepth 3 -maxdepth 3 -type d -name "merge_json" -print0 | sort -z)
|
||||
333
tools/temporal_analysis/track_objects_exported_onnx_infer_case.sh
Executable file
333
tools/temporal_analysis/track_objects_exported_onnx_infer_case.sh
Executable file
@@ -0,0 +1,333 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
shopt -s nullglob
|
||||
|
||||
# 用法说明:
|
||||
# 适配 run_two_roi_exported_onnx_infer_case.sh 的输出目录,对每个 case 执行:
|
||||
# {case_dir}/predictions/roi0/ -> {case_dir}/roi0.json
|
||||
# {case_dir}/predictions/roi1/ -> {case_dir}/roi1.json
|
||||
# {case_dir}/predictions/merge/ -> {case_dir}/merge.json
|
||||
# 并继续合并为:
|
||||
# {case_dir}/combined_tracking.json
|
||||
#
|
||||
# 支持三种模式:
|
||||
# 1) 批量模式(默认):遍历 RESULTS_ROOT 下所有包含 predictions/{roi0,roi1,merge} 的 case
|
||||
# bash track_objects_exported_onnx_infer_case.sh
|
||||
# 2) 指定根目录模式:第一个参数传推理输出根目录
|
||||
# bash track_objects_exported_onnx_infer_case.sh /path/to/inference_output_root
|
||||
# 3) 单 case 模式:第一个参数传 case 目录,或直接传 case_dir/predictions
|
||||
# bash track_objects_exported_onnx_infer_case.sh /path/to/one_case
|
||||
# bash track_objects_exported_onnx_infer_case.sh /path/to/one_case/predictions
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
export PYTHONPATH="${PROJECT_ROOT}:${PYTHONPATH:-}"
|
||||
|
||||
PYTHON_BIN="${PYTHON_BIN:-/deeplearning_team/ydong/dongying/miniconda/envs/dev/bin/python}"
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 配置区:默认与 run_two_roi_exported_onnx_infer_case.sh 保持一致
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
# /data1/dongying/Mono3d/G1Q3/model_inference/KPI/OP_KPI_SCENE/model_20260317_with_cls
|
||||
|
||||
RESULTS_ROOT="${RESULTS_ROOT:-/data1/dongying/Mono3d/G1Q3/model_inference/KPI/OP_KPI_SCENE/model_20260423-raw_no_edge}"
|
||||
TRACK_CLASSES="${TRACK_CLASSES:-0 1 2 3 4 5 6 7 8 9 10 11 12}"
|
||||
# TRACK_CLASSES="${TRACK_CLASSES:-0 1 2 3}"
|
||||
IOU_THRESH="${IOU_THRESH:-0.3}"
|
||||
MAX_AGE="${MAX_AGE:-5}"
|
||||
MIN_HITS="${MIN_HITS:-1}"
|
||||
DIST_THRESH="${DIST_THRESH:-100}"
|
||||
MAX_3D_DISTANCE="${MAX_3D_DISTANCE:-10.0}"
|
||||
MAX_FRAMES="${MAX_FRAMES:-}"
|
||||
MODEL_VERSION="${MODEL_VERSION:-20260423}"
|
||||
MERGE_OUTPUT_NAME="${MERGE_OUTPUT_NAME:-combined_tracking.json}"
|
||||
FILE_PATTERN="${FILE_PATTERN:-*.json}"
|
||||
ENABLE_USE_3D="${ENABLE_USE_3D:-0}"
|
||||
ENABLE_VRU_TRACKING="${ENABLE_VRU_TRACKING:-1}"
|
||||
VRU_TRACKING_OUTPUT_NAME="${VRU_TRACKING_OUTPUT_NAME:-merge_vru.json}"
|
||||
VRU_COMBINED_OUTPUT_NAME="${VRU_COMBINED_OUTPUT_NAME:-combined_tracking_with_vru.json}"
|
||||
TRACK_PARALLEL_JOBS="${TRACK_PARALLEL_JOBS:-1}"
|
||||
|
||||
echo ""
|
||||
echo "######################################################################"
|
||||
echo "# Tracking launcher for exported case/eval-root inference outputs"
|
||||
echo "######################################################################"
|
||||
|
||||
MAX_FRAMES_ARGS=()
|
||||
if [[ -n "${MAX_FRAMES}" ]]; then
|
||||
MAX_FRAMES_ARGS+=(--max-frames "${MAX_FRAMES}")
|
||||
fi
|
||||
|
||||
MODEL_VERSION_ARGS=()
|
||||
if [[ -n "${MODEL_VERSION}" ]]; then
|
||||
MODEL_VERSION_ARGS+=(--model-version "${MODEL_VERSION}")
|
||||
fi
|
||||
|
||||
USE_3D_ARGS=()
|
||||
if [[ "${ENABLE_USE_3D}" == "1" ]]; then
|
||||
USE_3D_ARGS+=(--use-3d --max-3d-distance "${MAX_3D_DISTANCE}")
|
||||
fi
|
||||
|
||||
is_predictions_dir() {
|
||||
local dir_path="$1"
|
||||
[[ -d "${dir_path}" ]] && [[ -d "${dir_path}/roi0" || -d "${dir_path}/roi1" || -d "${dir_path}/merge" ]]
|
||||
}
|
||||
|
||||
is_case_dir() {
|
||||
local dir_path="$1"
|
||||
is_predictions_dir "${dir_path}/predictions"
|
||||
}
|
||||
|
||||
has_json_files() {
|
||||
local dir_path="$1"
|
||||
[[ -d "${dir_path}" ]] && compgen -G "${dir_path}/${FILE_PATTERN}" > /dev/null
|
||||
}
|
||||
|
||||
resolve_case_dir() {
|
||||
local target_path="$1"
|
||||
if is_case_dir "${target_path}"; then
|
||||
printf '%s\n' "${target_path}"
|
||||
return 0
|
||||
fi
|
||||
if is_predictions_dir "${target_path}"; then
|
||||
dirname "${target_path}"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
emit_batch_case_dirs() {
|
||||
local batch_root="$1"
|
||||
local -a case_dirs=()
|
||||
|
||||
for first_level_dir in "${batch_root}"/*; do
|
||||
[[ -d "${first_level_dir}" ]] || continue
|
||||
|
||||
if is_case_dir "${first_level_dir}"; then
|
||||
case_dirs+=("${first_level_dir}")
|
||||
continue
|
||||
fi
|
||||
|
||||
for second_level_dir in "${first_level_dir}"/*; do
|
||||
[[ -d "${second_level_dir}" ]] || continue
|
||||
if is_case_dir "${second_level_dir}"; then
|
||||
case_dirs+=("${second_level_dir}")
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
if [[ "${#case_dirs[@]}" -gt 0 ]]; then
|
||||
printf '%s\0' "${case_dirs[@]}" | sort -z
|
||||
fi
|
||||
}
|
||||
|
||||
prune_active_pids() {
|
||||
local -n pids_ref=$1
|
||||
local -a remaining=()
|
||||
local pid
|
||||
|
||||
for pid in "${pids_ref[@]}"; do
|
||||
if kill -0 "${pid}" 2>/dev/null; then
|
||||
remaining+=("${pid}")
|
||||
fi
|
||||
done
|
||||
|
||||
pids_ref=("${remaining[@]}")
|
||||
}
|
||||
|
||||
run_tracking_for_source() {
|
||||
local input_dir="$1"
|
||||
local output_file="$2"
|
||||
local source_name="$3"
|
||||
shift 3
|
||||
local extra_tracking_args=("$@")
|
||||
|
||||
if [[ ! -d "${input_dir}" ]]; then
|
||||
echo "Warning: ${input_dir} not found, skipping ${source_name}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- Tracking ${source_name} ---"
|
||||
"${PYTHON_BIN}" "${PROJECT_ROOT}/tools/temporal_analysis/track_objects.py" \
|
||||
--input-dir "${input_dir}" \
|
||||
--output "${output_file}" \
|
||||
--file-pattern "${FILE_PATTERN}" \
|
||||
--classes ${TRACK_CLASSES} \
|
||||
--iou-threshold "${IOU_THRESH}" \
|
||||
--max-age "${MAX_AGE}" \
|
||||
--min-hits "${MIN_HITS}" \
|
||||
--distance-threshold "${DIST_THRESH}" \
|
||||
"${MAX_FRAMES_ARGS[@]}" \
|
||||
"${MODEL_VERSION_ARGS[@]}" \
|
||||
"${USE_3D_ARGS[@]}" \
|
||||
"${extra_tracking_args[@]}"
|
||||
}
|
||||
|
||||
run_single_case() {
|
||||
local case_dir="$1"
|
||||
local predictions_dir="${case_dir}/predictions"
|
||||
local has_input_source=0
|
||||
local merge_args=()
|
||||
|
||||
echo ""
|
||||
echo "Case: ${case_dir}"
|
||||
|
||||
if [[ -d "${predictions_dir}/roi0" ]]; then
|
||||
run_tracking_for_source "${predictions_dir}/roi0" "${case_dir}/roi0.json" "roi0"
|
||||
merge_args+=(--roi0 "${case_dir}/roi0.json")
|
||||
has_input_source=1
|
||||
else
|
||||
echo "Warning: ${predictions_dir}/roi0 not found, roi0 will not participate in merge"
|
||||
fi
|
||||
|
||||
if [[ -d "${predictions_dir}/roi1" ]]; then
|
||||
run_tracking_for_source "${predictions_dir}/roi1" "${case_dir}/roi1.json" "roi1"
|
||||
merge_args+=(--roi1 "${case_dir}/roi1.json")
|
||||
has_input_source=1
|
||||
else
|
||||
echo "Warning: ${predictions_dir}/roi1 not found, roi1 will not participate in merge"
|
||||
fi
|
||||
|
||||
if [[ -d "${predictions_dir}/merge" ]]; then
|
||||
run_tracking_for_source "${predictions_dir}/merge" "${case_dir}/merge.json" "merge"
|
||||
merge_args+=(--merge "${case_dir}/merge.json")
|
||||
has_input_source=1
|
||||
else
|
||||
echo "Warning: ${predictions_dir}/merge not found, merge will not participate in merge"
|
||||
fi
|
||||
|
||||
if [[ "${has_input_source}" -eq 0 ]]; then
|
||||
echo "Warning: no tracking input directories were found for ${case_dir}, skipping merge"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- Merging tracking results ---"
|
||||
"${PYTHON_BIN}" "${PROJECT_ROOT}/tools/temporal_analysis/merge_tracking_results.py" \
|
||||
"${merge_args[@]}" \
|
||||
--output "${case_dir}/${MERGE_OUTPUT_NAME}"
|
||||
|
||||
if [[ "${ENABLE_VRU_TRACKING}" == "1" ]] && has_json_files "${predictions_dir}/merge_vru"; then
|
||||
run_tracking_for_source \
|
||||
"${predictions_dir}/merge_vru" \
|
||||
"${case_dir}/${VRU_TRACKING_OUTPUT_NAME}" \
|
||||
"merge_vru" \
|
||||
--association-mode vru
|
||||
|
||||
if [[ -f "${case_dir}/${VRU_TRACKING_OUTPUT_NAME}" ]]; then
|
||||
echo ""
|
||||
echo "--- Merging tracking results with VRU source ---"
|
||||
"${PYTHON_BIN}" "${PROJECT_ROOT}/tools/temporal_analysis/merge_tracking_results.py" \
|
||||
"${merge_args[@]}" \
|
||||
--extra-source "merge_vru=${case_dir}/${VRU_TRACKING_OUTPUT_NAME}" \
|
||||
--output "${case_dir}/${VRU_COMBINED_OUTPUT_NAME}"
|
||||
else
|
||||
echo "Warning: VRU tracking output was not created, skipping ${VRU_COMBINED_OUTPUT_NAME}"
|
||||
fi
|
||||
elif [[ "${ENABLE_VRU_TRACKING}" == "1" ]]; then
|
||||
echo "Info: ${predictions_dir}/merge_vru has no ${FILE_PATTERN} files, skipping VRU tracking"
|
||||
fi
|
||||
}
|
||||
|
||||
run_batch_root() {
|
||||
local batch_root="$1"
|
||||
local parallel_jobs="${TRACK_PARALLEL_JOBS}"
|
||||
local total_cases=0
|
||||
local success_cases=0
|
||||
local failed_cases=0
|
||||
local status_dir
|
||||
local -a active_pids=()
|
||||
|
||||
echo "Batch-root mode: ${batch_root}"
|
||||
if ! [[ "${parallel_jobs}" =~ ^[0-9]+$ ]] || [[ "${parallel_jobs}" -lt 1 ]]; then
|
||||
echo "[ERROR] TRACK_PARALLEL_JOBS must be an integer >= 1, got: ${parallel_jobs}" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "Parallel jobs : ${parallel_jobs}"
|
||||
|
||||
status_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "${status_dir}"' RETURN
|
||||
|
||||
while IFS= read -r -d '' case_dir; do
|
||||
if ! is_case_dir "${case_dir}"; then
|
||||
continue
|
||||
fi
|
||||
((total_cases+=1))
|
||||
|
||||
if [[ "${parallel_jobs}" == "1" ]]; then
|
||||
if run_single_case "${case_dir}"; then
|
||||
((success_cases+=1))
|
||||
else
|
||||
((failed_cases+=1))
|
||||
printf '[FAIL] case=%s\n' "${case_dir}" >&2
|
||||
fi
|
||||
else
|
||||
(
|
||||
if run_single_case "${case_dir}"; then
|
||||
printf 'success\t%s\n' "${case_dir}" > "${status_dir}/$(printf '%05d' "${total_cases}").status"
|
||||
else
|
||||
printf 'failed\t%s\n' "${case_dir}" > "${status_dir}/$(printf '%05d' "${total_cases}").status"
|
||||
exit 1
|
||||
fi
|
||||
) &
|
||||
active_pids+=("$!")
|
||||
|
||||
while [[ "${#active_pids[@]}" -ge "${parallel_jobs}" ]]; do
|
||||
wait -n || true
|
||||
prune_active_pids active_pids
|
||||
done
|
||||
fi
|
||||
done < <(emit_batch_case_dirs "${batch_root}")
|
||||
|
||||
if [[ "${parallel_jobs}" != "1" ]]; then
|
||||
local pid
|
||||
local status_file
|
||||
local status
|
||||
local case_dir
|
||||
|
||||
for pid in "${active_pids[@]}"; do
|
||||
wait "${pid}" || true
|
||||
done
|
||||
|
||||
for status_file in "${status_dir}"/*.status; do
|
||||
[[ -f "${status_file}" ]] || continue
|
||||
IFS=$'\t' read -r status case_dir < "${status_file}"
|
||||
if [[ "${status}" == "success" ]]; then
|
||||
((success_cases+=1))
|
||||
else
|
||||
((failed_cases+=1))
|
||||
printf '[FAIL] case=%s\n' "${case_dir}" >&2
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $((success_cases + failed_cases)) -ne "${total_cases}" ]]; then
|
||||
failed_cases=$((total_cases - success_cases))
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "${total_cases}" -eq 0 ]]; then
|
||||
echo "[ERROR] No case directories containing predictions/{roi0,roi1,merge} were found under: ${batch_root}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
printf '[DONE] cases=%d success=%d failed=%d\n' \
|
||||
"${total_cases}" "${success_cases}" "${failed_cases}"
|
||||
|
||||
[[ "${failed_cases}" -eq 0 ]]
|
||||
}
|
||||
|
||||
TARGET_PATH="${1:-${RESULTS_ROOT}}"
|
||||
|
||||
if [[ ! -d "${TARGET_PATH}" ]]; then
|
||||
echo "Error: target directory does not exist: ${TARGET_PATH}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if RESOLVED_CASE_DIR="$(resolve_case_dir "${TARGET_PATH}")"; then
|
||||
run_single_case "${RESOLVED_CASE_DIR}"
|
||||
else
|
||||
run_batch_root "${TARGET_PATH}"
|
||||
fi
|
||||
239
tools/temporal_analysis/track_objects_exported_onnx_infer_cncap_json.sh
Executable file
239
tools/temporal_analysis/track_objects_exported_onnx_infer_cncap_json.sh
Executable file
@@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
shopt -s nullglob
|
||||
|
||||
# 用法说明:
|
||||
# 适配 run_two_roi_exported_onnx_infer_cncap_json.sh 的输出目录,对每个 case 执行:
|
||||
# {case_dir}/predictions/roi0/ -> {case_dir}/roi0.json
|
||||
# {case_dir}/predictions/roi1/ -> {case_dir}/roi1.json
|
||||
# {case_dir}/predictions/merge/ -> {case_dir}/merge.json
|
||||
# 并继续合并为:
|
||||
# {case_dir}/combined_tracking.json
|
||||
#
|
||||
# 支持三种模式:
|
||||
# 1) 批量模式(默认):遍历 RESULTS_ROOT 下所有包含 predictions/{roi0,roi1,merge} 的 case
|
||||
# bash track_objects_exported_onnx_infer_cncap_json.sh
|
||||
# 2) 指定根目录模式:第一个参数传推理输出根目录
|
||||
# bash track_objects_exported_onnx_infer_cncap_json.sh /path/to/inference_output_root
|
||||
# 3) 单 case 模式:第一个参数传 case 目录,或直接传 case_dir/predictions
|
||||
# bash track_objects_exported_onnx_infer_cncap_json.sh /path/to/one_case
|
||||
# bash track_objects_exported_onnx_infer_cncap_json.sh /path/to/one_case/predictions
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
export PYTHONPATH="${PROJECT_ROOT}:${PYTHONPATH:-}"
|
||||
|
||||
PYTHON_BIN="${PYTHON_BIN:-/deeplearning_team/ydong/dongying/miniconda/envs/dev/bin/python}"
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 配置区:默认与 run_two_roi_exported_onnx_infer_cncap_json.sh 保持一致
|
||||
# -----------------------------------------------------------------------
|
||||
RESULTS_ROOT="${RESULTS_ROOT:-/data1/dongying/Mono3d/G1Q3/model_inference/cncap/cncap_2024_with_cls_ego/model_20260407}"
|
||||
TRACK_CLASSES="${TRACK_CLASSES:-0 1 2 3 4 5 6 7 8 9 10 11 12}"
|
||||
IOU_THRESH="${IOU_THRESH:-0.3}"
|
||||
MAX_AGE="${MAX_AGE:-5}"
|
||||
MIN_HITS="${MIN_HITS:-1}"
|
||||
DIST_THRESH="${DIST_THRESH:-100}"
|
||||
MAX_3D_DISTANCE="${MAX_3D_DISTANCE:-10.0}"
|
||||
MAX_FRAMES="${MAX_FRAMES:-}"
|
||||
MODEL_VERSION="${MODEL_VERSION:-}"
|
||||
MERGE_OUTPUT_NAME="${MERGE_OUTPUT_NAME:-combined_tracking.json}"
|
||||
FILE_PATTERN="${FILE_PATTERN:-*.json}"
|
||||
ENABLE_USE_3D="${ENABLE_USE_3D:-0}"
|
||||
ENABLE_VRU_TRACKING="${ENABLE_VRU_TRACKING:-1}"
|
||||
VRU_TRACKING_OUTPUT_NAME="${VRU_TRACKING_OUTPUT_NAME:-merge_vru.json}"
|
||||
VRU_COMBINED_OUTPUT_NAME="${VRU_COMBINED_OUTPUT_NAME:-combined_tracking_with_vru.json}"
|
||||
|
||||
echo ""
|
||||
echo "######################################################################"
|
||||
echo "# Tracking launcher for exported CNCAP inference outputs"
|
||||
echo "######################################################################"
|
||||
|
||||
MAX_FRAMES_ARGS=()
|
||||
if [[ -n "${MAX_FRAMES}" ]]; then
|
||||
MAX_FRAMES_ARGS+=(--max-frames "${MAX_FRAMES}")
|
||||
fi
|
||||
|
||||
MODEL_VERSION_ARGS=()
|
||||
if [[ -n "${MODEL_VERSION}" ]]; then
|
||||
MODEL_VERSION_ARGS+=(--model-version "${MODEL_VERSION}")
|
||||
fi
|
||||
|
||||
USE_3D_ARGS=()
|
||||
if [[ "${ENABLE_USE_3D}" == "1" ]]; then
|
||||
USE_3D_ARGS+=(--use-3d --max-3d-distance "${MAX_3D_DISTANCE}")
|
||||
fi
|
||||
|
||||
is_predictions_dir() {
|
||||
local dir_path="$1"
|
||||
[[ -d "${dir_path}" ]] && [[ -d "${dir_path}/roi0" || -d "${dir_path}/roi1" || -d "${dir_path}/merge" ]]
|
||||
}
|
||||
|
||||
is_case_dir() {
|
||||
local dir_path="$1"
|
||||
is_predictions_dir "${dir_path}/predictions"
|
||||
}
|
||||
|
||||
has_json_files() {
|
||||
local dir_path="$1"
|
||||
[[ -d "${dir_path}" ]] && compgen -G "${dir_path}/${FILE_PATTERN}" > /dev/null
|
||||
}
|
||||
|
||||
resolve_case_dir() {
|
||||
local target_path="$1"
|
||||
if is_case_dir "${target_path}"; then
|
||||
printf '%s\n' "${target_path}"
|
||||
return 0
|
||||
fi
|
||||
if is_predictions_dir "${target_path}"; then
|
||||
dirname "${target_path}"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
run_tracking_for_source() {
|
||||
local input_dir="$1"
|
||||
local output_file="$2"
|
||||
local source_name="$3"
|
||||
shift 3
|
||||
local extra_tracking_args=("$@")
|
||||
|
||||
if [[ ! -d "${input_dir}" ]]; then
|
||||
echo "Warning: ${input_dir} not found, skipping ${source_name}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- Tracking ${source_name} ---"
|
||||
"${PYTHON_BIN}" "${PROJECT_ROOT}/tools/temporal_analysis/track_objects.py" \
|
||||
--input-dir "${input_dir}" \
|
||||
--output "${output_file}" \
|
||||
--file-pattern "${FILE_PATTERN}" \
|
||||
--classes ${TRACK_CLASSES} \
|
||||
--iou-threshold "${IOU_THRESH}" \
|
||||
--max-age "${MAX_AGE}" \
|
||||
--min-hits "${MIN_HITS}" \
|
||||
--distance-threshold "${DIST_THRESH}" \
|
||||
"${MAX_FRAMES_ARGS[@]}" \
|
||||
"${MODEL_VERSION_ARGS[@]}" \
|
||||
"${USE_3D_ARGS[@]}" \
|
||||
"${extra_tracking_args[@]}"
|
||||
}
|
||||
|
||||
run_single_case() {
|
||||
local case_dir="$1"
|
||||
local predictions_dir="${case_dir}/predictions"
|
||||
local has_input_source=0
|
||||
local merge_args=()
|
||||
|
||||
echo ""
|
||||
echo "Case: ${case_dir}"
|
||||
|
||||
if [[ -d "${predictions_dir}/roi0" ]]; then
|
||||
run_tracking_for_source "${predictions_dir}/roi0" "${case_dir}/roi0.json" "roi0"
|
||||
merge_args+=(--roi0 "${case_dir}/roi0.json")
|
||||
has_input_source=1
|
||||
else
|
||||
echo "Warning: ${predictions_dir}/roi0 not found, roi0 will not participate in merge"
|
||||
fi
|
||||
|
||||
if [[ -d "${predictions_dir}/roi1" ]]; then
|
||||
run_tracking_for_source "${predictions_dir}/roi1" "${case_dir}/roi1.json" "roi1"
|
||||
merge_args+=(--roi1 "${case_dir}/roi1.json")
|
||||
has_input_source=1
|
||||
else
|
||||
echo "Warning: ${predictions_dir}/roi1 not found, roi1 will not participate in merge"
|
||||
fi
|
||||
|
||||
if [[ -d "${predictions_dir}/merge" ]]; then
|
||||
run_tracking_for_source "${predictions_dir}/merge" "${case_dir}/merge.json" "merge"
|
||||
merge_args+=(--merge "${case_dir}/merge.json")
|
||||
has_input_source=1
|
||||
else
|
||||
echo "Warning: ${predictions_dir}/merge not found, merge will not participate in merge"
|
||||
fi
|
||||
|
||||
if [[ "${has_input_source}" -eq 0 ]]; then
|
||||
echo "Warning: no tracking input directories were found for ${case_dir}, skipping merge"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- Merging tracking results ---"
|
||||
"${PYTHON_BIN}" "${PROJECT_ROOT}/tools/temporal_analysis/merge_tracking_results.py" \
|
||||
"${merge_args[@]}" \
|
||||
--output "${case_dir}/${MERGE_OUTPUT_NAME}"
|
||||
|
||||
if [[ "${ENABLE_VRU_TRACKING}" == "1" ]] && has_json_files "${predictions_dir}/merge_vru"; then
|
||||
run_tracking_for_source \
|
||||
"${predictions_dir}/merge_vru" \
|
||||
"${case_dir}/${VRU_TRACKING_OUTPUT_NAME}" \
|
||||
"merge_vru" \
|
||||
--association-mode vru
|
||||
|
||||
if [[ -f "${case_dir}/${VRU_TRACKING_OUTPUT_NAME}" ]]; then
|
||||
echo ""
|
||||
echo "--- Merging tracking results with VRU source ---"
|
||||
"${PYTHON_BIN}" "${PROJECT_ROOT}/tools/temporal_analysis/merge_tracking_results.py" \
|
||||
"${merge_args[@]}" \
|
||||
--extra-source "merge_vru=${case_dir}/${VRU_TRACKING_OUTPUT_NAME}" \
|
||||
--output "${case_dir}/${VRU_COMBINED_OUTPUT_NAME}"
|
||||
else
|
||||
echo "Warning: VRU tracking output was not created, skipping ${VRU_COMBINED_OUTPUT_NAME}"
|
||||
fi
|
||||
elif [[ "${ENABLE_VRU_TRACKING}" == "1" ]]; then
|
||||
echo "Info: ${predictions_dir}/merge_vru has no ${FILE_PATTERN} files, skipping VRU tracking"
|
||||
fi
|
||||
}
|
||||
|
||||
run_batch_root() {
|
||||
local batch_root="$1"
|
||||
local total_cases=0
|
||||
local success_cases=0
|
||||
local failed_cases=0
|
||||
|
||||
echo "Batch-root mode: ${batch_root}"
|
||||
|
||||
while IFS= read -r -d '' predictions_dir; do
|
||||
local case_dir
|
||||
if ! is_predictions_dir "${predictions_dir}"; then
|
||||
continue
|
||||
fi
|
||||
case_dir="$(dirname "${predictions_dir}")"
|
||||
((total_cases+=1))
|
||||
|
||||
if run_single_case "${case_dir}"; then
|
||||
((success_cases+=1))
|
||||
else
|
||||
((failed_cases+=1))
|
||||
printf '[FAIL] case=%s\n' "${case_dir}" >&2
|
||||
fi
|
||||
done < <(
|
||||
find "${batch_root}" -type d -name predictions -print0 | sort -z
|
||||
)
|
||||
|
||||
if [[ "${total_cases}" -eq 0 ]]; then
|
||||
echo "[ERROR] No case directories containing predictions/{roi0,roi1,merge} were found under: ${batch_root}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
printf '[DONE] cases=%d success=%d failed=%d\n' \
|
||||
"${total_cases}" "${success_cases}" "${failed_cases}"
|
||||
|
||||
[[ "${failed_cases}" -eq 0 ]]
|
||||
}
|
||||
|
||||
TARGET_PATH="${1:-${RESULTS_ROOT}}"
|
||||
|
||||
if [[ ! -d "${TARGET_PATH}" ]]; then
|
||||
echo "Error: target directory does not exist: ${TARGET_PATH}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if RESOLVED_CASE_DIR="$(resolve_case_dir "${TARGET_PATH}")"; then
|
||||
run_single_case "${RESOLVED_CASE_DIR}"
|
||||
else
|
||||
run_batch_root "${TARGET_PATH}"
|
||||
fi
|
||||
187
tools/temporal_analysis/track_objects_exported_onnx_infer_event_json.sh
Executable file
187
tools/temporal_analysis/track_objects_exported_onnx_infer_event_json.sh
Executable file
@@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
shopt -s nullglob
|
||||
|
||||
# 用法说明:
|
||||
# 适配 run_two_roi_exported_onnx_infer_event_json.sh 的输出目录。
|
||||
# 与按 clip 跟踪不同,这里会对同一个 event_id 目录下所有 clip 的逐帧 JSON
|
||||
# 先按文件名中的时间戳进行全局排序,再执行一次 event 级跟踪。
|
||||
#
|
||||
# 输出目录约定:
|
||||
# 默认自动解析每个 event 对应的 datename(优先取 _status/event_manifest.json,
|
||||
# 再回退 scene 级 _status/scene_event_manifest.json),输出为:
|
||||
# {event_dir}/{datename}/roi0.json
|
||||
# {event_dir}/{datename}/roi1.json
|
||||
# {event_dir}/{datename}/merge.json
|
||||
# {event_dir}/{datename}/combined_tracking.json
|
||||
# {event_dir}/{datename}/frame_order_manifest.json
|
||||
#
|
||||
# 如需强制使用固定目录名,可设置:
|
||||
# EVENT_TRACKING_DIRNAME=event_tracking
|
||||
#
|
||||
# 支持三种模式:
|
||||
# 1) 批量模式(默认):遍历 RESULTS_ROOT 下所有 event 目录
|
||||
# bash track_objects_exported_onnx_infer_event_json.sh
|
||||
# 2) 指定根目录或 scene 目录:
|
||||
# bash track_objects_exported_onnx_infer_event_json.sh /path/to/event_results_root
|
||||
# bash track_objects_exported_onnx_infer_event_json.sh /path/to/event_results_root/scene_xxx
|
||||
# 3) 单 event 模式:
|
||||
# bash track_objects_exported_onnx_infer_event_json.sh /path/to/event_results_root/scene_xxx/event_id_xxx
|
||||
# bash track_objects_exported_onnx_infer_event_json.sh /path/to/event_results_root/scene_xxx/rawid_xxx
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
export PYTHONPATH="${PROJECT_ROOT}:${PYTHONPATH:-}"
|
||||
|
||||
PYTHON_BIN="${PYTHON_BIN:-/deeplearning_team/ydong/dongying/miniconda/envs/dev/bin/python}"
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 配置区:默认与 run_two_roi_exported_onnx_infer_event_json.sh 保持一致
|
||||
# -----------------------------------------------------------------------
|
||||
RESULTS_ROOT="${RESULTS_ROOT:-/data1/dongying/Mono3d/G1Q3/model_inference/cncap/cncap_2021_with_cls_ego/model_20260416}"
|
||||
TRACK_CLASSES="${TRACK_CLASSES:-0 1 2 3 4 5 6 7 8 9 10 11 12}"
|
||||
IOU_THRESH="${IOU_THRESH:-0.3}"
|
||||
MAX_AGE="${MAX_AGE:-5}"
|
||||
MIN_HITS="${MIN_HITS:-1}"
|
||||
DIST_THRESH="${DIST_THRESH:-100}"
|
||||
MAX_3D_DISTANCE="${MAX_3D_DISTANCE:-10.0}"
|
||||
MODEL_VERSION="${MODEL_VERSION:-20260416}"
|
||||
FILE_PATTERN="${FILE_PATTERN:-*.json}"
|
||||
ENABLE_USE_3D="${ENABLE_USE_3D:-0}"
|
||||
MERGE_OUTPUT_NAME="${MERGE_OUTPUT_NAME:-combined_tracking.json}"
|
||||
MANIFEST_NAME="${MANIFEST_NAME:-frame_order_manifest.json}"
|
||||
EVENT_TRACKING_DIRNAME="${EVENT_TRACKING_DIRNAME:-}"
|
||||
SCENE_FILTER="${SCENE_FILTER:-}"
|
||||
EVENT_FILTER="${EVENT_FILTER:-}"
|
||||
|
||||
echo ""
|
||||
echo "######################################################################"
|
||||
echo "# Event-level tracking launcher for exported event-json inference"
|
||||
echo "######################################################################"
|
||||
|
||||
MODEL_VERSION_ARGS=()
|
||||
if [[ -n "${MODEL_VERSION}" ]]; then
|
||||
MODEL_VERSION_ARGS+=(--model-version "${MODEL_VERSION}")
|
||||
fi
|
||||
|
||||
USE_3D_ARGS=()
|
||||
if [[ "${ENABLE_USE_3D}" == "1" ]]; then
|
||||
USE_3D_ARGS+=(--use-3d --max-3d-distance "${MAX_3D_DISTANCE}")
|
||||
fi
|
||||
|
||||
is_event_dir() {
|
||||
local dir_path="$1"
|
||||
[[ -d "${dir_path}" ]] || return 1
|
||||
|
||||
if [[ -f "${dir_path}/_status/event_manifest.json" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local predictions_dir
|
||||
for predictions_dir in "${dir_path}"/*/predictions; do
|
||||
[[ -d "${predictions_dir}" ]] || continue
|
||||
return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
emit_batch_event_dirs() {
|
||||
local batch_root="$1"
|
||||
local manifest_path
|
||||
|
||||
{
|
||||
while IFS= read -r -d '' manifest_path; do
|
||||
printf '%s\0' "$(dirname "$(dirname "${manifest_path}")")"
|
||||
done < <(find "${batch_root}" -type f -path "*/_status/event_manifest.json" -print0)
|
||||
|
||||
# Backward-compatible fallback for older event-json outputs that may
|
||||
# not have event manifests yet but still use the historical prefix.
|
||||
find "${batch_root}" -type d -name "event_id_*" -print0
|
||||
} | sort -zu
|
||||
}
|
||||
|
||||
run_single_event() {
|
||||
local event_dir="$1"
|
||||
local output_dir_args=()
|
||||
|
||||
echo ""
|
||||
echo "Event: ${event_dir}"
|
||||
|
||||
if [[ -n "${EVENT_TRACKING_DIRNAME}" ]]; then
|
||||
output_dir_args=(--output-dir "${event_dir}/${EVENT_TRACKING_DIRNAME}")
|
||||
fi
|
||||
|
||||
"${PYTHON_BIN}" "${PROJECT_ROOT}/tools/temporal_analysis/track_event_objects.py" \
|
||||
--event-dir "${event_dir}" \
|
||||
"${output_dir_args[@]}" \
|
||||
--file-pattern "${FILE_PATTERN}" \
|
||||
--classes ${TRACK_CLASSES} \
|
||||
--iou-threshold "${IOU_THRESH}" \
|
||||
--max-age "${MAX_AGE}" \
|
||||
--min-hits "${MIN_HITS}" \
|
||||
--distance-threshold "${DIST_THRESH}" \
|
||||
--merge-output-name "${MERGE_OUTPUT_NAME}" \
|
||||
--manifest-name "${MANIFEST_NAME}" \
|
||||
"${MODEL_VERSION_ARGS[@]}" \
|
||||
"${USE_3D_ARGS[@]}"
|
||||
}
|
||||
|
||||
run_batch_root() {
|
||||
local batch_root="$1"
|
||||
local total_events=0
|
||||
local success_events=0
|
||||
local failed_events=0
|
||||
|
||||
echo "Batch-root mode: ${batch_root}"
|
||||
[[ -n "${SCENE_FILTER}" ]] && echo "Scene filter: ${SCENE_FILTER}"
|
||||
[[ -n "${EVENT_FILTER}" ]] && echo "Event filter: ${EVENT_FILTER}"
|
||||
|
||||
while IFS= read -r -d '' event_dir; do
|
||||
local scene_name
|
||||
local event_name
|
||||
scene_name="$(basename "$(dirname "${event_dir}")")"
|
||||
event_name="$(basename "${event_dir}")"
|
||||
|
||||
if [[ -n "${SCENE_FILTER}" ]] && [[ ! "${scene_name}" =~ ${SCENE_FILTER} ]]; then
|
||||
continue
|
||||
fi
|
||||
if [[ -n "${EVENT_FILTER}" ]] && [[ ! "${event_name}" =~ ${EVENT_FILTER} ]]; then
|
||||
continue
|
||||
fi
|
||||
if ! is_event_dir "${event_dir}"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
((total_events+=1))
|
||||
if run_single_event "${event_dir}"; then
|
||||
((success_events+=1))
|
||||
else
|
||||
((failed_events+=1))
|
||||
printf '[FAIL] scene=%s event=%s\n' "${scene_name}" "${event_name}" >&2
|
||||
fi
|
||||
done < <(emit_batch_event_dirs "${batch_root}")
|
||||
|
||||
if [[ "${total_events}" -eq 0 ]]; then
|
||||
echo "[ERROR] No event directories were found under: ${batch_root}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
printf '[DONE] events=%d success=%d failed=%d\n' \
|
||||
"${total_events}" "${success_events}" "${failed_events}"
|
||||
|
||||
[[ "${failed_events}" -eq 0 ]]
|
||||
}
|
||||
|
||||
TARGET_PATH="${1:-${RESULTS_ROOT}}"
|
||||
|
||||
if [[ ! -d "${TARGET_PATH}" ]]; then
|
||||
echo "Error: target directory does not exist: ${TARGET_PATH}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if is_event_dir "${TARGET_PATH}"; then
|
||||
run_single_event "${TARGET_PATH}"
|
||||
else
|
||||
run_batch_root "${TARGET_PATH}"
|
||||
fi
|
||||
88
tools/temporal_analysis/visualize_tracking_batch.sh
Executable file
88
tools/temporal_analysis/visualize_tracking_batch.sh
Executable file
@@ -0,0 +1,88 @@
|
||||
#!/bin/bash
|
||||
# 批量将跟踪结果可视化到视频帧上。
|
||||
#
|
||||
# 流程(每个 case):
|
||||
# 直接读取 camera4.bin,将 merge_tracking.json 叠加到每帧,保存可视化图片目录或 mp4。
|
||||
# 无需保存中间帧,节省磁盘空间。
|
||||
#
|
||||
# 目录约定(与 test_merged_model.sh 保持一致):
|
||||
# CASES_ROOT/.../{case_id}/{sub}/sigmastar.1/camera4.bin ← 原始视频
|
||||
# RESULTS_ROOT/MODEL_NAME/{run_name}/merge_tracking.json ← 跟踪结果
|
||||
# RESULTS_ROOT/MODEL_NAME/{run_name}/tracking_vis.mp4 ← 可视化输出(SAVE_VIDEO=true)
|
||||
# RESULTS_ROOT/MODEL_NAME/{run_name}/tracking_vis/ ← 可视化图片(SAVE_VIDEO=false)
|
||||
|
||||
# Set PYTHONPATH to project root for module imports
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
export PYTHONPATH="${PROJECT_ROOT}:${PYTHONPATH}"
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 配置区:与 test_merged_model.sh / track_objects.sh 保持一致
|
||||
# -----------------------------------------------------------------------
|
||||
CASES_ROOT="/data1/dongying/Mono3d/G1M3/cases_coding"
|
||||
RESULTS_ROOT="/data1/dongying/Mono3d/G1M3/cases_coding_results"
|
||||
MODEL_NAME="torchscript_python_300w_newdata-cncap-v2"
|
||||
|
||||
# 可视化选项
|
||||
SAVE_VIDEO=false # true=保存 mp4;false=保存图片目录
|
||||
VIS_FPS=25 # 输出视频帧率
|
||||
VIS_CLASS_ID="" # 过滤类别,例如 "--class-id 0";留空则显示全部类别
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 批量循环:与 test_merged_model.sh 相同的路径推导逻辑
|
||||
# -----------------------------------------------------------------------
|
||||
while IFS= read -r -d '' camera4_bin; do
|
||||
sigmastar_dir=$(dirname "$camera4_bin")
|
||||
|
||||
rel_path="${sigmastar_dir#"$CASES_ROOT"/}"
|
||||
rel_path="${rel_path%/sigmastar.1}"
|
||||
run_name=$(echo "$rel_path" | tr '/ ' '__')
|
||||
|
||||
case_dir="${RESULTS_ROOT}/${MODEL_NAME}/${run_name}"
|
||||
tracking_file="${case_dir}/merge_tracking.json"
|
||||
|
||||
# 跟踪结果不存在则跳过(该 case 尚未跑 track_objects.sh)
|
||||
if [[ ! -f "$tracking_file" ]]; then
|
||||
echo "[SKIP] No merge_tracking.json: $case_dir"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Case : $run_name"
|
||||
echo "Video : $camera4_bin"
|
||||
echo "Track : $tracking_file"
|
||||
echo "=========================================="
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 直接读取视频并可视化(不保存中间帧)
|
||||
# ------------------------------------------------------------------
|
||||
if [[ "$SAVE_VIDEO" == true ]]; then
|
||||
output_arg="${case_dir}/tracking_vis.mp4"
|
||||
save_video_flag="--save-video"
|
||||
echo "[INFO] Visualizing → $output_arg"
|
||||
else
|
||||
output_arg="${case_dir}/tracking_vis"
|
||||
save_video_flag=""
|
||||
echo "[INFO] Visualizing → $output_arg/"
|
||||
fi
|
||||
|
||||
python "${PROJECT_ROOT}/tools/temporal_analysis/visualize_tracking_boxes.py" \
|
||||
--tracking "$tracking_file" \
|
||||
--images "$camera4_bin" \
|
||||
--output "$output_arg" \
|
||||
--fps "$VIS_FPS" \
|
||||
$save_video_flag \
|
||||
$VIS_CLASS_ID
|
||||
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "[ERROR] Visualization failed for case: $run_name"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "[DONE] $run_name"
|
||||
|
||||
done < <(find "$CASES_ROOT" -name "camera4.bin" -print0 | sort -z)
|
||||
|
||||
echo ""
|
||||
echo "All cases processed."
|
||||
685
tools/temporal_analysis/visualize_tracking_boxes.py
Executable file
685
tools/temporal_analysis/visualize_tracking_boxes.py
Executable file
@@ -0,0 +1,685 @@
|
||||
"""
|
||||
Visualize tracking results by drawing 2D boxes on images.
|
||||
|
||||
Usage:
|
||||
# 从图像文件夹可视化
|
||||
python visualize_tracking_boxes.py \
|
||||
--tracking runs/val_viz/exp/tracking.json \
|
||||
--images path/to/images \
|
||||
--output runs/val_viz/exp/tracked_images
|
||||
|
||||
# 生成视频
|
||||
python visualize_tracking_boxes.py \
|
||||
--tracking runs/val_viz/exp/tracking.json \
|
||||
--images path/to/images \
|
||||
--output runs/val_viz/exp/tracked.mp4 \
|
||||
--save-video
|
||||
|
||||
# 只可视化前100帧
|
||||
python visualize_tracking_boxes.py \
|
||||
--tracking tracking.json \
|
||||
--images images/ \
|
||||
--output output/ \
|
||||
--max-frames 100
|
||||
|
||||
# 按 frameId 对齐原始 camera4.bin,并导出单帧图片
|
||||
python visualize_tracking_boxes.py \
|
||||
--tracking merge.json \
|
||||
--images /path/to/case/sigmastar.1/camera4.bin \
|
||||
--output tracking_vis_raw \
|
||||
--align-by-frame-id
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
import cv2
|
||||
import numpy as np
|
||||
from tqdm import tqdm
|
||||
import colorsys
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from tools.model_inference.adapters.video_dir_inference_utils import (
|
||||
iter_video_case_frames,
|
||||
read_video_frame_index,
|
||||
)
|
||||
|
||||
|
||||
def generate_colors(n):
|
||||
"""生成n个区分度高的颜色"""
|
||||
colors = []
|
||||
for i in range(n):
|
||||
hue = i / n
|
||||
saturation = 0.9
|
||||
value = 0.9
|
||||
rgb = colorsys.hsv_to_rgb(hue, saturation, value)
|
||||
colors.append(tuple(int(x * 255) for x in rgb))
|
||||
return colors
|
||||
|
||||
|
||||
def get_color_for_track(track_id, color_map):
|
||||
"""为track_id分配一个持久的颜色"""
|
||||
if track_id not in color_map:
|
||||
# 使用track_id作为种子生成颜色
|
||||
np.random.seed(track_id)
|
||||
color = (
|
||||
np.random.randint(50, 255),
|
||||
np.random.randint(50, 255),
|
||||
np.random.randint(50, 255)
|
||||
)
|
||||
color_map[track_id] = color
|
||||
return color_map[track_id]
|
||||
|
||||
|
||||
def should_visualize_detection(det, class_id_filter=None, track_ids_filter=None):
|
||||
"""判断检测是否应该被可视化
|
||||
|
||||
Args:
|
||||
det: 检测结果字典
|
||||
class_id_filter: 指定的类别ID,None表示所有类别
|
||||
track_ids_filter: 指定的track ID列表,None表示所有ID
|
||||
|
||||
Returns:
|
||||
bool: 是否应该可视化该检测
|
||||
"""
|
||||
# 检查class_id过滤
|
||||
if class_id_filter is not None and det.get('class_id') != class_id_filter:
|
||||
return False
|
||||
|
||||
# 检查track_id过滤
|
||||
if track_ids_filter is not None:
|
||||
track_id = det.get('track_id')
|
||||
if track_id not in track_ids_filter:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def safe_int(value):
|
||||
"""Best-effort integer conversion."""
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def extract_frame_id_from_image_name(image_name):
|
||||
"""Best-effort frame-id extraction from tracking image_name."""
|
||||
stem = Path(str(image_name or "")).stem
|
||||
match = re.search(r"_(\d+)_(\d+)(?:_merged)?$", stem)
|
||||
if match is not None:
|
||||
return int(match.group(1))
|
||||
|
||||
match = re.search(r"_(\d+)(?:_merged)?$", stem)
|
||||
if match is not None:
|
||||
return int(match.group(1))
|
||||
|
||||
match = re.search(r"(\d+)", stem)
|
||||
if match is not None:
|
||||
return int(match.group(1))
|
||||
return None
|
||||
|
||||
|
||||
def extract_tracking_frame_id(frame_data, fallback_idx=None):
|
||||
"""Resolve one frame's frameId from detections, frame_info, or image_name."""
|
||||
for det in frame_data.get("detections", []):
|
||||
for key in ("frameId", "frame_id"):
|
||||
frame_id = safe_int(det.get(key))
|
||||
if frame_id is not None:
|
||||
return frame_id
|
||||
|
||||
frame_info = frame_data.get("frame_info")
|
||||
if isinstance(frame_info, dict):
|
||||
for key in ("original_frame_id", "frame_id", "frameId"):
|
||||
frame_id = safe_int(frame_info.get(key))
|
||||
if frame_id is not None:
|
||||
return frame_id
|
||||
|
||||
frame_id = extract_frame_id_from_image_name(frame_data.get("image_name"))
|
||||
if frame_id is not None:
|
||||
return frame_id
|
||||
return fallback_idx
|
||||
|
||||
|
||||
def resolve_detection_class_name(det, class_id, class_names):
|
||||
"""Prefer detection-provided type_name before falling back to static names."""
|
||||
type_name = str(det.get("type_name") or "").strip()
|
||||
if type_name:
|
||||
return type_name
|
||||
return class_names.get(class_id, f"Class{class_id}")
|
||||
|
||||
|
||||
def build_output_frame_name(frame_data, fallback_idx):
|
||||
"""Build a stable output filename for one visualized frame."""
|
||||
frame_name = frame_data.get("image_name", f"frame_{fallback_idx:06d}")
|
||||
stem = Path(str(frame_name)).stem
|
||||
return f"{stem}.jpg"
|
||||
|
||||
|
||||
def draw_box_with_label(img, bbox, track_id, class_id, confidence, color, class_names, class_name_override=None):
|
||||
"""在图像上绘制边界框和标签"""
|
||||
x1, y1, x2, y2 = map(int, bbox)
|
||||
|
||||
# 绘制边界框
|
||||
thickness = 2
|
||||
cv2.rectangle(img, (x1, y1), (x2, y2), color, thickness)
|
||||
|
||||
# 准备标签文本
|
||||
class_name = class_name_override or class_names.get(class_id, f'Class{class_id}')
|
||||
label = f'ID:{track_id} {class_name} {confidence:.2f}'
|
||||
|
||||
# 计算标签背景大小
|
||||
font = cv2.FONT_HERSHEY_SIMPLEX
|
||||
font_scale = 0.6
|
||||
font_thickness = 2
|
||||
(text_width, text_height), baseline = cv2.getTextSize(label, font, font_scale, font_thickness)
|
||||
|
||||
# 绘制标签背景
|
||||
label_y1 = max(y1 - text_height - 10, 0)
|
||||
label_y2 = y1
|
||||
cv2.rectangle(img, (x1, label_y1), (x1 + text_width + 10, label_y2), color, -1)
|
||||
|
||||
# 绘制标签文本
|
||||
text_x = x1 + 5
|
||||
text_y = label_y1 + text_height + 5
|
||||
cv2.putText(img, label, (text_x, text_y), font, font_scale, (255, 255, 255), font_thickness)
|
||||
|
||||
return img
|
||||
|
||||
|
||||
def render_tracking_frame(
|
||||
img,
|
||||
frame_data,
|
||||
frame_idx,
|
||||
total_frames,
|
||||
color_map,
|
||||
class_names,
|
||||
trajectory_history=None,
|
||||
class_id_filter=None,
|
||||
track_ids_filter=None,
|
||||
):
|
||||
"""Render one tracking frame on top of an image."""
|
||||
detections = frame_data.get('detections', [])
|
||||
visualized_count = 0
|
||||
for det in detections:
|
||||
track_id = det.get('track_id')
|
||||
if track_id is None:
|
||||
continue
|
||||
|
||||
if not should_visualize_detection(det, class_id_filter, track_ids_filter):
|
||||
continue
|
||||
|
||||
visualized_count += 1
|
||||
bbox = det['bbox']
|
||||
class_id = det['class_id']
|
||||
confidence = det['confidence']
|
||||
color = get_color_for_track(track_id, color_map)
|
||||
class_name = resolve_detection_class_name(det, class_id, class_names)
|
||||
|
||||
img = draw_box_with_label(
|
||||
img,
|
||||
bbox,
|
||||
track_id,
|
||||
class_id,
|
||||
confidence,
|
||||
color,
|
||||
class_names,
|
||||
class_name_override=class_name,
|
||||
)
|
||||
|
||||
if trajectory_history is not None:
|
||||
center = ((bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2)
|
||||
if track_id not in trajectory_history:
|
||||
trajectory_history[track_id] = []
|
||||
trajectory_history[track_id].append(center)
|
||||
|
||||
if len(trajectory_history[track_id]) > 1:
|
||||
points = np.array(trajectory_history[track_id], dtype=np.int32)
|
||||
for i in range(len(points) - 1):
|
||||
cv2.line(img, tuple(points[i]), tuple(points[i + 1]), color, 2)
|
||||
|
||||
if class_id_filter is not None or track_ids_filter is not None:
|
||||
info_text = f"Frame: {frame_idx + 1}/{total_frames} | Showing: {visualized_count}/{len(detections)}"
|
||||
else:
|
||||
info_text = f"Frame: {frame_idx + 1}/{total_frames} | Tracks: {len(detections)}"
|
||||
cv2.putText(img, info_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.8, (0, 255, 0), 2)
|
||||
return img, visualized_count
|
||||
|
||||
|
||||
def visualize_tracking_on_raw_video_by_frame_id(
|
||||
tracking_data,
|
||||
video_path,
|
||||
output_path,
|
||||
max_frames=None,
|
||||
show_trajectory=False,
|
||||
class_id_filter=None,
|
||||
track_ids_filter=None,
|
||||
):
|
||||
"""Visualize tracking results on raw camera4.bin frames using frameId alignment."""
|
||||
video_path = Path(video_path)
|
||||
output_path = Path(output_path)
|
||||
output_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
class_names = {
|
||||
0: 'Car',
|
||||
1: 'Pedestrian',
|
||||
2: 'Tricycle',
|
||||
3: 'Cyclist'
|
||||
}
|
||||
color_map = {}
|
||||
trajectory_history = {} if show_trajectory else None
|
||||
|
||||
selected_tracking_data = tracking_data[:max_frames] if max_frames else tracking_data
|
||||
frame_id_to_tracking = {}
|
||||
ordered_frame_ids = []
|
||||
skipped_duplicates = []
|
||||
unresolved_frames = []
|
||||
for idx, frame_data in enumerate(selected_tracking_data):
|
||||
frame_id = extract_tracking_frame_id(frame_data, fallback_idx=idx)
|
||||
if frame_id is None:
|
||||
unresolved_frames.append(frame_data.get("image_name", f"frame_{idx:06d}"))
|
||||
continue
|
||||
if frame_id in frame_id_to_tracking:
|
||||
skipped_duplicates.append(frame_id)
|
||||
continue
|
||||
frame_id_to_tracking[frame_id] = (idx, frame_data)
|
||||
ordered_frame_ids.append(frame_id)
|
||||
|
||||
if not frame_id_to_tracking:
|
||||
print("Error: no frameIds could be resolved from tracking data")
|
||||
return
|
||||
|
||||
frame_index_payload = read_video_frame_index(video_path)
|
||||
total_targets = len(ordered_frame_ids)
|
||||
matched_frame_ids = set()
|
||||
|
||||
print(f"Reading raw video with frameId alignment: {video_path}")
|
||||
print(f"Saving aligned images to: {output_path}")
|
||||
if unresolved_frames:
|
||||
print(f"Warning: skipped {len(unresolved_frames)} frame(s) without resolvable frameId")
|
||||
if skipped_duplicates:
|
||||
print(f"Warning: skipped duplicate frameId(s): {sorted(set(skipped_duplicates))[:10]}")
|
||||
|
||||
progress = tqdm(total=total_targets, desc="Visualizing")
|
||||
for read_frame_index, frame_bgr, _, frame_info in iter_video_case_frames(
|
||||
video_path,
|
||||
frame_index_payload=frame_index_payload,
|
||||
frame_stride=1,
|
||||
max_frames=0,
|
||||
):
|
||||
aligned_frame_id = safe_int(None if frame_info is None else frame_info.get("frame_id"))
|
||||
if aligned_frame_id is None:
|
||||
aligned_frame_id = read_frame_index
|
||||
|
||||
if aligned_frame_id not in frame_id_to_tracking:
|
||||
continue
|
||||
|
||||
tracking_idx, frame_data = frame_id_to_tracking[aligned_frame_id]
|
||||
rendered, _ = render_tracking_frame(
|
||||
frame_bgr.copy(),
|
||||
frame_data,
|
||||
tracking_idx,
|
||||
total_targets,
|
||||
color_map,
|
||||
class_names,
|
||||
trajectory_history=trajectory_history,
|
||||
class_id_filter=class_id_filter,
|
||||
track_ids_filter=track_ids_filter,
|
||||
)
|
||||
output_file = output_path / build_output_frame_name(frame_data, tracking_idx)
|
||||
cv2.imwrite(str(output_file), rendered)
|
||||
matched_frame_ids.add(aligned_frame_id)
|
||||
progress.update(1)
|
||||
|
||||
if len(matched_frame_ids) >= total_targets:
|
||||
break
|
||||
|
||||
progress.close()
|
||||
|
||||
missing_frame_ids = [frame_id for frame_id in ordered_frame_ids if frame_id not in matched_frame_ids]
|
||||
print(f"\n✓ Visualization complete!")
|
||||
print(f" Images saved to: {output_path}")
|
||||
print(f" Matched frames: {len(matched_frame_ids)}/{total_targets}")
|
||||
print(f" Total tracks visualized: {len(color_map)}")
|
||||
if missing_frame_ids:
|
||||
preview = ", ".join(str(frame_id) for frame_id in missing_frame_ids[:10])
|
||||
suffix = "..." if len(missing_frame_ids) > 10 else ""
|
||||
print(f" Missing frameIds: {preview}{suffix}")
|
||||
|
||||
|
||||
def visualize_tracking_on_images(tracking_data, image_source, output_path,
|
||||
save_video=False, fps=25, max_frames=None,
|
||||
show_trajectory=False, class_id_filter=None,
|
||||
track_ids_filter=None, align_by_frame_id=False):
|
||||
"""将跟踪结果可视化到图像上
|
||||
|
||||
Args:
|
||||
tracking_data: 跟踪结果数据(从tracking.json加载)
|
||||
image_source: 图像来源(文件夹路径或视频文件路径)
|
||||
output_path: 输出路径(文件夹或视频文件)
|
||||
save_video: 是否保存为视频
|
||||
fps: 视频帧率
|
||||
max_frames: 最大处理帧数
|
||||
show_trajectory: 是否显示轨迹线
|
||||
class_id_filter: 指定类别ID,None表示所有类别
|
||||
track_ids_filter: 指定track ID集合,None表示所有ID
|
||||
align_by_frame_id: 是否按 frameId 对齐原始视频帧
|
||||
"""
|
||||
image_source = Path(image_source)
|
||||
output_path = Path(output_path)
|
||||
|
||||
if align_by_frame_id:
|
||||
if save_video:
|
||||
print("Error: --align-by-frame-id only supports image-directory output")
|
||||
return
|
||||
if not image_source.is_file():
|
||||
print("Error: --align-by-frame-id requires a video file such as camera4.bin")
|
||||
return
|
||||
visualize_tracking_on_raw_video_by_frame_id(
|
||||
tracking_data,
|
||||
image_source,
|
||||
output_path,
|
||||
max_frames=max_frames,
|
||||
show_trajectory=show_trajectory,
|
||||
class_id_filter=class_id_filter,
|
||||
track_ids_filter=track_ids_filter,
|
||||
)
|
||||
return
|
||||
|
||||
# 类别名称
|
||||
class_names = {
|
||||
0: 'Car',
|
||||
1: 'Pedestrian',
|
||||
2: 'Tricycle',
|
||||
3: 'Cyclist'
|
||||
}
|
||||
|
||||
# 颜色映射
|
||||
color_map = {}
|
||||
|
||||
# 轨迹历史(用于绘制轨迹线)
|
||||
trajectory_history = {} if show_trajectory else None
|
||||
|
||||
# 检查图像源类型
|
||||
VIDEO_EXTENSIONS = {'.mp4', '.avi', '.mov', '.mkv', '.bin'}
|
||||
if image_source.is_file() and image_source.suffix.lower() in VIDEO_EXTENSIONS:
|
||||
# 从视频读取
|
||||
cap = cv2.VideoCapture(str(image_source))
|
||||
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||
frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||||
frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||
print(f"Reading from video: {image_source}")
|
||||
print(f"Video info: {frame_width}x{frame_height}, {total_frames} frames")
|
||||
elif image_source.is_dir():
|
||||
# 从图像文件夹读取
|
||||
cap = None
|
||||
image_files = sorted(list(image_source.glob('*.jpg')) +
|
||||
list(image_source.glob('*.png')) +
|
||||
list(image_source.glob('*.jpeg')))
|
||||
if not image_files:
|
||||
print(f"Error: No images found in {image_source}")
|
||||
return
|
||||
|
||||
# 读取第一张图像获取尺寸
|
||||
first_img = cv2.imread(str(image_files[0]))
|
||||
if first_img is None:
|
||||
print(f"Error: Cannot read image {image_files[0]}")
|
||||
return
|
||||
frame_height, frame_width = first_img.shape[:2]
|
||||
total_frames = len(image_files)
|
||||
print(f"Reading from image folder: {image_source}")
|
||||
print(f"Found {total_frames} images, size: {frame_width}x{frame_height}")
|
||||
else:
|
||||
print(f"Error: Invalid image source: {image_source}")
|
||||
return
|
||||
|
||||
# 限制处理帧数
|
||||
if max_frames:
|
||||
total_frames = min(total_frames, max_frames)
|
||||
tracking_data = tracking_data[:total_frames]
|
||||
|
||||
# 准备输出
|
||||
if save_video:
|
||||
if not str(output_path).endswith(('.mp4', '.avi')):
|
||||
output_path = output_path.with_suffix('.mp4')
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
||||
video_writer = cv2.VideoWriter(str(output_path), fourcc, fps, (frame_width, frame_height))
|
||||
print(f"Saving video to: {output_path}")
|
||||
else:
|
||||
output_path.mkdir(parents=True, exist_ok=True)
|
||||
video_writer = None
|
||||
print(f"Saving images to: {output_path}")
|
||||
|
||||
# 处理每一帧
|
||||
print(f"\nProcessing {len(tracking_data)} frames...")
|
||||
for frame_idx, frame_data in enumerate(tqdm(tracking_data, desc="Visualizing")):
|
||||
# 读取图像
|
||||
if cap is not None:
|
||||
ret, img = cap.read()
|
||||
if not ret:
|
||||
print(f"Warning: Cannot read frame {frame_idx}")
|
||||
break
|
||||
else:
|
||||
if frame_idx >= len(image_files):
|
||||
break
|
||||
img = cv2.imread(str(image_files[frame_idx]))
|
||||
if img is None:
|
||||
print(f"Warning: Cannot read image {image_files[frame_idx]}")
|
||||
continue
|
||||
|
||||
img, _ = render_tracking_frame(
|
||||
img,
|
||||
frame_data,
|
||||
frame_idx,
|
||||
len(tracking_data),
|
||||
color_map,
|
||||
class_names,
|
||||
trajectory_history=trajectory_history,
|
||||
class_id_filter=class_id_filter,
|
||||
track_ids_filter=track_ids_filter,
|
||||
)
|
||||
|
||||
# 保存或写入视频
|
||||
if video_writer:
|
||||
video_writer.write(img)
|
||||
else:
|
||||
frame_name = frame_data.get('image_name', f'frame_{frame_idx:06d}.jpg')
|
||||
if not frame_name.endswith(('.jpg', '.png', '.jpeg')):
|
||||
frame_name = f'{Path(frame_name).stem}.jpg'
|
||||
output_file = output_path / frame_name.replace('.png', '.jpg')
|
||||
cv2.imwrite(str(output_file), img)
|
||||
|
||||
# 清理
|
||||
if cap is not None:
|
||||
cap.release()
|
||||
if video_writer is not None:
|
||||
video_writer.release()
|
||||
|
||||
print(f"\n✓ Visualization complete!")
|
||||
if save_video:
|
||||
print(f" Video saved to: {output_path}")
|
||||
else:
|
||||
print(f" Images saved to: {output_path}")
|
||||
print(f" Total tracks visualized: {len(color_map)}")
|
||||
|
||||
|
||||
def create_side_by_side_comparison(tracking_file1, tracking_file2, image_source,
|
||||
output_path, labels=None, max_frames=100):
|
||||
"""创建并排对比可视化
|
||||
|
||||
Args:
|
||||
tracking_file1, tracking_file2: 两个跟踪结果文件
|
||||
image_source: 图像源
|
||||
output_path: 输出视频路径
|
||||
labels: 两个方法的标签
|
||||
max_frames: 最大处理帧数
|
||||
"""
|
||||
if labels is None:
|
||||
labels = ['Method 1', 'Method 2']
|
||||
|
||||
# 加载跟踪数据
|
||||
with open(tracking_file1, 'r') as f:
|
||||
data1 = json.load(f)
|
||||
with open(tracking_file2, 'r') as f:
|
||||
data2 = json.load(f)
|
||||
|
||||
image_source = Path(image_source)
|
||||
output_path = Path(output_path)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
class_names = {0: 'Car', 1: 'Pedestrian', 2: 'Tricycle', 3: 'Cyclist'}
|
||||
color_map1, color_map2 = {}, {}
|
||||
|
||||
# 读取图像
|
||||
if image_source.is_dir():
|
||||
image_files = sorted(list(image_source.glob('*.jpg')) +
|
||||
list(image_source.glob('*.png')))
|
||||
first_img = cv2.imread(str(image_files[0]))
|
||||
h, w = first_img.shape[:2]
|
||||
else:
|
||||
cap = cv2.VideoCapture(str(image_source))
|
||||
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||||
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||
cap.release()
|
||||
|
||||
# 创建视频写入器(并排宽度加倍)
|
||||
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
||||
video_writer = cv2.VideoWriter(str(output_path), fourcc, 25, (w * 2, h))
|
||||
|
||||
print(f"Creating side-by-side comparison video...")
|
||||
frames_to_process = min(len(data1), len(data2), max_frames)
|
||||
|
||||
for frame_idx in tqdm(range(frames_to_process), desc="Processing"):
|
||||
# 读取原始图像
|
||||
if image_source.is_dir():
|
||||
img = cv2.imread(str(image_files[frame_idx]))
|
||||
else:
|
||||
cap = cv2.VideoCapture(str(image_source))
|
||||
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
|
||||
ret, img = cap.read()
|
||||
cap.release()
|
||||
if not ret:
|
||||
break
|
||||
|
||||
img1 = img.copy()
|
||||
img2 = img.copy()
|
||||
|
||||
# 绘制第一个跟踪结果
|
||||
for det in data1[frame_idx].get('detections', []):
|
||||
if 'track_id' in det:
|
||||
color = get_color_for_track(det['track_id'], color_map1)
|
||||
img1 = draw_box_with_label(img1, det['bbox'], det['track_id'],
|
||||
det['class_id'], det['confidence'], color, class_names)
|
||||
|
||||
# 绘制第二个跟踪结果
|
||||
for det in data2[frame_idx].get('detections', []):
|
||||
if 'track_id' in det:
|
||||
color = get_color_for_track(det['track_id'], color_map2)
|
||||
img2 = draw_box_with_label(img2, det['bbox'], det['track_id'],
|
||||
det['class_id'], det['confidence'], color, class_names)
|
||||
|
||||
# 添加标签
|
||||
cv2.putText(img1, labels[0], (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
|
||||
cv2.putText(img2, labels[1], (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
|
||||
|
||||
# 并排拼接
|
||||
combined = np.hstack([img1, img2])
|
||||
video_writer.write(combined)
|
||||
|
||||
video_writer.release()
|
||||
print(f"✓ Comparison video saved to: {output_path}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Visualize tracking results on images')
|
||||
parser.add_argument('--tracking', type=str, required=True, help='Path to tracking.json file')
|
||||
parser.add_argument('--images', type=str, required=True, help='Path to images folder or video file')
|
||||
parser.add_argument('--output', type=str, required=True, help='Output path (folder or video file)')
|
||||
parser.add_argument('--save-video', action='store_true', help='Save as video instead of images')
|
||||
parser.add_argument('--fps', type=int, default=25, help='Video FPS (default: 25)')
|
||||
parser.add_argument('--max-frames', type=int, help='Maximum number of frames to process')
|
||||
parser.add_argument('--show-trajectory', action='store_true', help='Show trajectory lines')
|
||||
parser.add_argument('--compare', type=str, help='Second tracking.json for side-by-side comparison')
|
||||
parser.add_argument('--labels', type=str, nargs=2, default=['Method 1', 'Method 2'],
|
||||
help='Labels for comparison mode')
|
||||
parser.add_argument('--class-id', type=int, default=None,
|
||||
help='Filter by class ID (0=Car, 1=Pedestrian, 2=Cyclist, etc.). If not specified, all classes are shown.')
|
||||
parser.add_argument('--track-ids', type=int, nargs='+', default=None,
|
||||
help='Specific track IDs to visualize (e.g., --track-ids 1 2 3). If not specified, all tracks are shown.')
|
||||
parser.add_argument('--align-by-frame-id', action='store_true',
|
||||
help='Align tracking frames to the raw input video by frameId and export images only.')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 检查输入文件
|
||||
tracking_path = Path(args.tracking)
|
||||
if not tracking_path.exists():
|
||||
print(f"Error: Tracking file not found: {tracking_path}")
|
||||
return
|
||||
|
||||
image_source = Path(args.images)
|
||||
if not image_source.exists():
|
||||
print(f"Error: Image source not found: {image_source}")
|
||||
return
|
||||
|
||||
# 加载跟踪数据
|
||||
print(f"Loading tracking data from {tracking_path}")
|
||||
with open(tracking_path, 'r') as f:
|
||||
tracking_data = json.load(f)
|
||||
|
||||
print(f"Loaded {len(tracking_data)} frames")
|
||||
|
||||
# 对比模式
|
||||
if args.compare:
|
||||
compare_path = Path(args.compare)
|
||||
if not compare_path.exists():
|
||||
print(f"Error: Comparison file not found: {compare_path}")
|
||||
return
|
||||
|
||||
output_path = Path(args.output)
|
||||
if not str(output_path).endswith('.mp4'):
|
||||
output_path = output_path.with_suffix('.mp4')
|
||||
|
||||
create_side_by_side_comparison(
|
||||
tracking_path, compare_path, image_source,
|
||||
output_path, args.labels, args.max_frames or 100
|
||||
)
|
||||
else:
|
||||
# 单文件可视化
|
||||
# 准备过滤参数
|
||||
track_ids_set = set(args.track_ids) if args.track_ids else None
|
||||
|
||||
# 打印过滤信息
|
||||
if args.class_id is not None or args.track_ids:
|
||||
print(f"\nFiltering settings:")
|
||||
if args.class_id is not None:
|
||||
print(f" Class ID: {args.class_id}")
|
||||
if args.track_ids:
|
||||
print(f" Track IDs: {args.track_ids}")
|
||||
|
||||
visualize_tracking_on_images(
|
||||
tracking_data,
|
||||
image_source,
|
||||
args.output,
|
||||
save_video=args.save_video,
|
||||
fps=args.fps,
|
||||
max_frames=args.max_frames,
|
||||
show_trajectory=args.show_trajectory,
|
||||
class_id_filter=args.class_id,
|
||||
track_ids_filter=track_ids_set,
|
||||
align_by_frame_id=args.align_by_frame_id,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
35
tools/temporal_analysis/visualize_tracking_boxes.sh
Executable file
35
tools/temporal_analysis/visualize_tracking_boxes.sh
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
# /data1/xdzhu/Testdata_0129/019b18ef-ca2b-7e97-8ca5-d4b1d7eefdbb/images/
|
||||
# /data1/xdzhu/Mono_G1M3/test_1616_video/异型车/2026-01-07_14-21-06_voice-左侧异型车/sigmastar.1/frames_camera4/
|
||||
# /data1/xdzhu/Mono_G1M3/test_1616_video/异型车/2026-01-07_13-27-03_voice-异型车/sigmastar.1/frames_camera4/
|
||||
# /data1/dongying/Mono3d/G1M3/eval_dataset/20251210222153/sigmastar.1/frames_camera4
|
||||
|
||||
# python ./eval_tools/temporal_analysis/visualize_tracking_boxes.py \
|
||||
# --tracking runs/val_viz_300w/20251210222153_roi0/tracking.json \
|
||||
# --images /data1/dongying/Mono3d/G1M3/eval_dataset/20251210222153/sigmastar.1/frames_camera4 \
|
||||
# --output runs/val_viz_300w/20251210222153_roi0/tracking_vis \
|
||||
# --class-id 0
|
||||
# --track-ids 2 41 102
|
||||
|
||||
# # --save-video
|
||||
|
||||
# Set PYTHONPATH to project root for module imports
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
export PYTHONPATH="${PROJECT_ROOT}:${PYTHONPATH}"
|
||||
|
||||
|
||||
# python ./eval_tools/temporal_analysis/visualize_tracking_boxes.py \
|
||||
# --tracking runs/val_viz/2026-01-07_14-21-06_roi1/tracking.json \
|
||||
# --images /data1/xdzhu/Mono_G1M3/test_1616_video/异型车/2026-01-07_14-21-06_voice-左侧异型车/sigmastar.1/frames_camera4/ \
|
||||
# --output runs/val_viz/2026-01-07_14-21-06_roi1/tracking_vis \
|
||||
# --class-id 0
|
||||
# # --track-ids 2 41 102
|
||||
# # --save-video
|
||||
|
||||
python ./tools/temporal_analysis/visualize_tracking_boxes.py \
|
||||
--tracking /data1/dongying/Mono3d/G1M3/test_outputs/video_case_model_20260416/20260416103945/merge.json \
|
||||
--images /data1/dongying/Mono3d/G1M3/test_data/cncap_20260414/20260416103945/sigmastar.1/camera4.bin \
|
||||
--output /data1/dongying/Mono3d/G1M3/test_outputs/video_case_model_20260416/20260416103945/tracking_vis \
|
||||
--class-id 9
|
||||
# --save-video
|
||||
337
tools/temporal_analysis/visualize_tracking_exported_onnx_infer_case.sh
Executable file
337
tools/temporal_analysis/visualize_tracking_exported_onnx_infer_case.sh
Executable file
@@ -0,0 +1,337 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Visualize tracking JSON produced by exported video-root inference outputs.
|
||||
#
|
||||
# For each case, the script reads:
|
||||
# {case_dir}/merge.json
|
||||
# and renders tracking boxes onto:
|
||||
# {VIDEO_ROOT_DIR}/{relative_case_dir}/sigmastar.1/camera4.bin
|
||||
# then writes:
|
||||
# {case_dir}/tracking_vis/
|
||||
#
|
||||
# Supported modes:
|
||||
# 1) Batch mode (default): walk RESULTS_ROOT and process all case dirs
|
||||
# containing TRACKING_JSON_NAME.
|
||||
# 2) Root mode: pass an exported-infer results root as the first argument.
|
||||
# 3) Single-case mode: pass a case dir or a direct tracking JSON path.
|
||||
#
|
||||
# Notes:
|
||||
# - The script uses --align-by-frame-id, so output is an image directory.
|
||||
# - TRACKING_JSON_NAME defaults to merge.json because combined_tracking.json
|
||||
# offsets track IDs by source and is less convenient for per-target review.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
export PYTHONPATH="${PROJECT_ROOT}:${PYTHONPATH:-}"
|
||||
|
||||
RESULTS_ROOT="${RESULTS_ROOT:-/data1/dongying/Mono3d/G1Q3/dataset_for_evaluation/CNCAP/CSTA_LN_outputs/model_20260416}"
|
||||
VIDEO_ROOT_DIR="${VIDEO_ROOT_DIR:-/data1/dongying/Mono3d/G1Q3/dataset_for_evaluation/CNCAP/CSTA_LN}"
|
||||
OUTPUT_ROOT="${OUTPUT_ROOT:-}"
|
||||
OUTPUT_DIR_NAME="${OUTPUT_DIR_NAME:-tracking_vis}"
|
||||
PYTHON_BIN="${PYTHON_BIN:-/deeplearning_team/ydong/dongying/miniconda/envs/dev/bin/python}"
|
||||
VISUALIZE_SCRIPT="${VISUALIZE_SCRIPT:-${PROJECT_ROOT}/tools/temporal_analysis/visualize_tracking_boxes.py}"
|
||||
TRACKING_JSON_NAME="${TRACKING_JSON_NAME:-merge.json}"
|
||||
VIS_MAX_FRAMES="${VIS_MAX_FRAMES:-}"
|
||||
VIS_CLASS_ID="${VIS_CLASS_ID:-}"
|
||||
VIS_TRACK_IDS="${VIS_TRACK_IDS:-}"
|
||||
VIS_SHOW_TRAJECTORY="${VIS_SHOW_TRAJECTORY:-0}"
|
||||
|
||||
TARGET_PATH=""
|
||||
CLI_CLASS_ID=""
|
||||
CLI_MAX_FRAMES=""
|
||||
CLI_SHOW_TRAJECTORY=0
|
||||
CLI_TRACK_IDS=()
|
||||
|
||||
while (($# > 0)); do
|
||||
case "$1" in
|
||||
--track-id)
|
||||
if (($# < 2)); then
|
||||
echo "Error: --track-id requires a value" >&2
|
||||
exit 1
|
||||
fi
|
||||
CLI_TRACK_IDS+=("$2")
|
||||
shift 2
|
||||
;;
|
||||
--track-id=*)
|
||||
CLI_TRACK_IDS+=("${1#*=}")
|
||||
shift
|
||||
;;
|
||||
--class-id)
|
||||
if (($# < 2)); then
|
||||
echo "Error: --class-id requires a value" >&2
|
||||
exit 1
|
||||
fi
|
||||
CLI_CLASS_ID="$2"
|
||||
shift 2
|
||||
;;
|
||||
--class-id=*)
|
||||
CLI_CLASS_ID="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--max-frames)
|
||||
if (($# < 2)); then
|
||||
echo "Error: --max-frames requires a value" >&2
|
||||
exit 1
|
||||
fi
|
||||
CLI_MAX_FRAMES="$2"
|
||||
shift 2
|
||||
;;
|
||||
--max-frames=*)
|
||||
CLI_MAX_FRAMES="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--show-trajectory)
|
||||
CLI_SHOW_TRAJECTORY=1
|
||||
shift
|
||||
;;
|
||||
-*)
|
||||
echo "Error: unsupported option: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
if [[ -n "${TARGET_PATH}" ]]; then
|
||||
echo "Error: multiple target paths provided: ${TARGET_PATH} and $1" >&2
|
||||
exit 1
|
||||
fi
|
||||
TARGET_PATH="$1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "${TARGET_PATH}" ]]; then
|
||||
TARGET_PATH="${RESULTS_ROOT}"
|
||||
fi
|
||||
|
||||
if [[ ! -e "${TARGET_PATH}" ]]; then
|
||||
echo "Error: target path does not exist: ${TARGET_PATH}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -d "${VIDEO_ROOT_DIR}" ]]; then
|
||||
echo "Error: VIDEO_ROOT_DIR does not exist: ${VIDEO_ROOT_DIR}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
resolve_abs_path() {
|
||||
local target_path="$1"
|
||||
if [[ -d "${target_path}" ]]; then
|
||||
(
|
||||
cd "${target_path}"
|
||||
pwd -P
|
||||
)
|
||||
return 0
|
||||
fi
|
||||
|
||||
local parent_dir
|
||||
parent_dir=$(
|
||||
cd "$(dirname "${target_path}")"
|
||||
pwd -P
|
||||
)
|
||||
printf '%s/%s\n' "${parent_dir}" "$(basename "${target_path}")"
|
||||
}
|
||||
|
||||
RESULTS_ROOT_ABS="$(resolve_abs_path "${RESULTS_ROOT}")"
|
||||
VIDEO_ROOT_DIR_ABS="$(resolve_abs_path "${VIDEO_ROOT_DIR}")"
|
||||
|
||||
is_case_dir() {
|
||||
local dir_path="$1"
|
||||
[[ -d "${dir_path}" ]] && [[ -f "${dir_path}/${TRACKING_JSON_NAME}" ]]
|
||||
}
|
||||
|
||||
resolve_case_dir() {
|
||||
local target_path="$1"
|
||||
if is_case_dir "${target_path}"; then
|
||||
printf '%s\n' "${target_path}"
|
||||
return 0
|
||||
fi
|
||||
if [[ -f "${target_path}" ]] && [[ "$(basename "${target_path}")" == "${TRACKING_JSON_NAME}" ]]; then
|
||||
dirname "${target_path}"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
derive_output_dir() {
|
||||
local case_dir="$1"
|
||||
local case_abs
|
||||
local rel_case_dir
|
||||
|
||||
case_abs="$(resolve_abs_path "${case_dir}")"
|
||||
if [[ -n "${OUTPUT_ROOT}" ]]; then
|
||||
if [[ "${case_abs}" == "${RESULTS_ROOT_ABS}" ]]; then
|
||||
printf '%s\n' "${OUTPUT_ROOT}"
|
||||
return 0
|
||||
fi
|
||||
if [[ "${case_abs}" == "${RESULTS_ROOT_ABS}"/* ]]; then
|
||||
rel_case_dir="${case_abs#"${RESULTS_ROOT_ABS}/"}"
|
||||
printf '%s/%s\n' "${OUTPUT_ROOT%/}" "${rel_case_dir}"
|
||||
return 0
|
||||
fi
|
||||
printf '%s/%s\n' "${OUTPUT_ROOT%/}" "$(basename "${case_abs}")"
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf '%s/%s\n' "${case_abs}" "${OUTPUT_DIR_NAME}"
|
||||
}
|
||||
|
||||
derive_raw_video_path() {
|
||||
local case_dir="$1"
|
||||
local case_abs
|
||||
local rel_case_dir
|
||||
|
||||
case_abs="$(resolve_abs_path "${case_dir}")"
|
||||
if [[ "${case_abs}" != "${RESULTS_ROOT_ABS}" && "${case_abs}" != "${RESULTS_ROOT_ABS}"/* ]]; then
|
||||
echo "Error: case directory is not under RESULTS_ROOT: ${case_abs}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ "${case_abs}" == "${RESULTS_ROOT_ABS}" ]]; then
|
||||
echo "Error: cannot derive a raw video path from the results root itself" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
rel_case_dir="${case_abs#"${RESULTS_ROOT_ABS}/"}"
|
||||
printf '%s/%s/sigmastar.1/camera4.bin\n' "${VIDEO_ROOT_DIR_ABS}" "${rel_case_dir}"
|
||||
}
|
||||
|
||||
build_track_id_args() {
|
||||
local -n out_ref=$1
|
||||
out_ref=()
|
||||
|
||||
if [[ "${#CLI_TRACK_IDS[@]}" -gt 0 ]]; then
|
||||
for track_id in "${CLI_TRACK_IDS[@]}"; do
|
||||
out_ref+=(--track-ids "${track_id}")
|
||||
done
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -n "${VIS_TRACK_IDS}" ]]; then
|
||||
# shellcheck disable=SC2206
|
||||
local track_id_arr=(${VIS_TRACK_IDS})
|
||||
for track_id in "${track_id_arr[@]}"; do
|
||||
out_ref+=(--track-ids "${track_id}")
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
run_single_case() {
|
||||
local case_dir="$1"
|
||||
local tracking_json="${case_dir}/${TRACKING_JSON_NAME}"
|
||||
local raw_video_path
|
||||
local output_dir
|
||||
local cmd
|
||||
local track_id_args=()
|
||||
local class_id_value=""
|
||||
local max_frames_value=""
|
||||
local show_trajectory_value=0
|
||||
|
||||
if [[ ! -f "${tracking_json}" ]]; then
|
||||
echo "[ERROR] ${TRACKING_JSON_NAME} not found in case directory: ${case_dir}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
raw_video_path="$(derive_raw_video_path "${case_dir}")"
|
||||
if [[ ! -f "${raw_video_path}" ]]; then
|
||||
echo "[ERROR] camera4.bin not found for case: ${case_dir}" >&2
|
||||
echo " expected: ${raw_video_path}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
output_dir="$(derive_output_dir "${case_dir}")"
|
||||
build_track_id_args track_id_args
|
||||
|
||||
if [[ -n "${CLI_CLASS_ID}" ]]; then
|
||||
class_id_value="${CLI_CLASS_ID}"
|
||||
elif [[ -n "${VIS_CLASS_ID}" ]]; then
|
||||
class_id_value="${VIS_CLASS_ID}"
|
||||
fi
|
||||
|
||||
if [[ -n "${CLI_MAX_FRAMES}" ]]; then
|
||||
max_frames_value="${CLI_MAX_FRAMES}"
|
||||
elif [[ -n "${VIS_MAX_FRAMES}" ]]; then
|
||||
max_frames_value="${VIS_MAX_FRAMES}"
|
||||
fi
|
||||
|
||||
if [[ "${CLI_SHOW_TRAJECTORY}" == "1" || "${VIS_SHOW_TRAJECTORY}" == "1" ]]; then
|
||||
show_trajectory_value=1
|
||||
fi
|
||||
|
||||
cmd=(
|
||||
"${PYTHON_BIN}" "${VISUALIZE_SCRIPT}"
|
||||
--tracking "${tracking_json}"
|
||||
--images "${raw_video_path}"
|
||||
--output "${output_dir}"
|
||||
--align-by-frame-id
|
||||
)
|
||||
|
||||
if [[ -n "${max_frames_value}" ]]; then
|
||||
cmd+=(--max-frames "${max_frames_value}")
|
||||
fi
|
||||
|
||||
if [[ -n "${class_id_value}" ]]; then
|
||||
cmd+=(--class-id "${class_id_value}")
|
||||
fi
|
||||
|
||||
if [[ "${#track_id_args[@]}" -gt 0 ]]; then
|
||||
cmd+=("${track_id_args[@]}")
|
||||
fi
|
||||
|
||||
if [[ "${show_trajectory_value}" == "1" ]]; then
|
||||
cmd+=(--show-trajectory)
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "######################################################################"
|
||||
echo "# Tracking visualization for exported video-root outputs"
|
||||
echo "######################################################################"
|
||||
echo "Case : ${case_dir}"
|
||||
echo "Track : ${tracking_json}"
|
||||
echo "Video : ${raw_video_path}"
|
||||
echo "Output : ${output_dir}"
|
||||
|
||||
"${cmd[@]}"
|
||||
}
|
||||
|
||||
run_batch_root() {
|
||||
local batch_root="$1"
|
||||
local total_cases=0
|
||||
local success_cases=0
|
||||
local failed_cases=0
|
||||
|
||||
while IFS= read -r -d '' tracking_json; do
|
||||
local case_dir
|
||||
case_dir="$(dirname "${tracking_json}")"
|
||||
((total_cases += 1))
|
||||
|
||||
if run_single_case "${case_dir}"; then
|
||||
((success_cases += 1))
|
||||
else
|
||||
((failed_cases += 1))
|
||||
printf '[FAIL] case=%s\n' "${case_dir}" >&2
|
||||
fi
|
||||
done < <(
|
||||
find "${batch_root}" -type f -name "${TRACKING_JSON_NAME}" -print0 | sort -z
|
||||
)
|
||||
|
||||
if [[ "${total_cases}" -eq 0 ]]; then
|
||||
echo "[ERROR] No ${TRACKING_JSON_NAME} files were found under: ${batch_root}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
printf '[DONE] cases=%d success=%d failed=%d\n' \
|
||||
"${total_cases}" "${success_cases}" "${failed_cases}"
|
||||
|
||||
[[ "${failed_cases}" -eq 0 ]]
|
||||
}
|
||||
|
||||
if RESOLVED_CASE_DIR="$(resolve_case_dir "${TARGET_PATH}")"; then
|
||||
run_single_case "${RESOLVED_CASE_DIR}"
|
||||
elif [[ -d "${TARGET_PATH}" ]]; then
|
||||
run_batch_root "${TARGET_PATH}"
|
||||
else
|
||||
echo "Error: unsupported target path: ${TARGET_PATH}" >&2
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user