单目3D初始代码
This commit is contained in:
1157
tools/analyze_mono3d_head_targets.py
Executable file
1157
tools/analyze_mono3d_head_targets.py
Executable file
File diff suppressed because it is too large
Load Diff
417
tools/clean_ground2d_dataset.py
Executable file
417
tools/clean_ground2d_dataset.py
Executable file
@@ -0,0 +1,417 @@
|
||||
#!/usr/bin/env python3
|
||||
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
||||
"""Clean ground 2D dataset labels and corrupt image pairs.
|
||||
|
||||
This script matches the duplicate-label definition used by
|
||||
``ultralytics.data.utils.verify_image_label_ground``:
|
||||
|
||||
[class_id, x_center, y_center, width, height, difficulty1 + difficulty2]
|
||||
|
||||
Rows that map to the same parsed tuple are duplicates; the first raw row is kept.
|
||||
By default the script is a dry run. Pass ``--apply`` to rewrite label files,
|
||||
delete corrupt image/label pairs, and update split list files.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
try:
|
||||
from tqdm import tqdm
|
||||
except ImportError:
|
||||
tqdm = None
|
||||
|
||||
IMG_FORMATS = {
|
||||
"avif",
|
||||
"bmp",
|
||||
"dng",
|
||||
"heic",
|
||||
"heif",
|
||||
"jp2",
|
||||
"jpeg",
|
||||
"jpeg2000",
|
||||
"jpg",
|
||||
"mpo",
|
||||
"png",
|
||||
"tif",
|
||||
"tiff",
|
||||
"webp",
|
||||
}
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--data",
|
||||
default="ultralytics/cfg/datasets/mono2d_ground.yaml",
|
||||
help="Ground 2D dataset YAML path.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--splits",
|
||||
nargs="+",
|
||||
default=["train", "val"],
|
||||
help="Dataset YAML split keys to scan, for example: train val test.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--apply",
|
||||
action="store_true",
|
||||
help="Actually modify files. Without this flag, only prints what would change.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-update-lists",
|
||||
action="store_true",
|
||||
help="Do not remove corrupt entries from split list files.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-backup",
|
||||
action="store_true",
|
||||
help="Do not create .bak files before rewriting labels/list files.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--report",
|
||||
default="",
|
||||
help="Optional report file path. Defaults to no report file.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--progress-interval",
|
||||
type=int,
|
||||
default=100,
|
||||
help="Print progress every N entries when tqdm is not installed. Set 0 to disable fallback progress.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def read_lines(path: Path) -> list[str]:
|
||||
return path.read_text(encoding="utf-8").splitlines()
|
||||
|
||||
|
||||
def strip_yaml_comment(value: str) -> str:
|
||||
"""Strip simple YAML comments while preserving path strings."""
|
||||
return value.split("#", 1)[0].strip()
|
||||
|
||||
|
||||
def parse_scalar(value: str) -> object:
|
||||
value = strip_yaml_comment(value)
|
||||
if not value:
|
||||
return None
|
||||
if value.lower() in {"true", "false"}:
|
||||
return value.lower() == "true"
|
||||
if value.startswith("[") and value.endswith("]"):
|
||||
items = [x.strip() for x in value[1:-1].split(",") if x.strip()]
|
||||
parsed = []
|
||||
for item in items:
|
||||
try:
|
||||
parsed.append(float(item))
|
||||
except ValueError:
|
||||
parsed.append(item.strip("'\""))
|
||||
return parsed
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
return value.strip("'\"")
|
||||
|
||||
|
||||
def load_ground_yaml(path: Path) -> dict[str, object]:
|
||||
"""Load the small YAML subset used by mono2d_ground.yaml without external dependencies."""
|
||||
data: dict[str, object] = {}
|
||||
current_map: str | None = None
|
||||
for raw_line in read_lines(path):
|
||||
if not raw_line.strip() or raw_line.lstrip().startswith("#"):
|
||||
continue
|
||||
indent = len(raw_line) - len(raw_line.lstrip(" "))
|
||||
line = raw_line.strip()
|
||||
if indent == 0:
|
||||
key, sep, value = line.partition(":")
|
||||
if not sep:
|
||||
continue
|
||||
parsed = parse_scalar(value)
|
||||
if parsed is None:
|
||||
data[key] = {}
|
||||
current_map = key
|
||||
else:
|
||||
data[key] = parsed
|
||||
current_map = None
|
||||
elif current_map and isinstance(data.get(current_map), dict):
|
||||
key, sep, value = line.partition(":")
|
||||
if sep:
|
||||
data[current_map][key.strip()] = parse_scalar(value)
|
||||
return data
|
||||
|
||||
|
||||
def img2label_path(img_path: str) -> str:
|
||||
sa, sb = f"{os.sep}images{os.sep}", f"{os.sep}labels{os.sep}"
|
||||
return sb.join(img_path.rsplit(sa, 1)).rsplit(".", 1)[0] + ".txt"
|
||||
|
||||
|
||||
def write_lines(path: Path, lines: list[str], backup: bool) -> None:
|
||||
if backup and path.exists():
|
||||
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
backup_path = path.with_name(f"{path.name}.bak.{stamp}")
|
||||
backup_path.write_text(path.read_text(encoding="utf-8"), encoding="utf-8")
|
||||
path.write_text("\n".join(lines) + ("\n" if lines else ""), encoding="utf-8")
|
||||
|
||||
|
||||
def split_entries(split_path: Path) -> list[str]:
|
||||
return [line.strip() for line in read_lines(split_path) if line.strip()]
|
||||
|
||||
|
||||
def expand_split_entry(entry: str, split_path: Path) -> str:
|
||||
"""Mirror YOLOGroundDataset.get_img_files list-file expansion."""
|
||||
if entry.startswith("./"):
|
||||
return entry.replace("./", str(split_path.parent) + os.sep, 1)
|
||||
return entry
|
||||
|
||||
|
||||
def resolve_split_path(value: str | None, data_dir: Path) -> Path | None:
|
||||
if not value:
|
||||
return None
|
||||
path = Path(value)
|
||||
return path if path.is_absolute() else data_dir / path
|
||||
|
||||
|
||||
def ground_image_path_from_label_path(path: str, image_root: Path, label_path: bool = False) -> Path:
|
||||
"""Mirror YOLOGroundDataset._ground_image_path_from_label_path."""
|
||||
parts = Path(path).parts
|
||||
detection_root_index = next(
|
||||
(i for i, part in enumerate(parts) if part.lower().startswith(("detection2d", "2ddetection"))),
|
||||
None,
|
||||
)
|
||||
if detection_root_index is not None and detection_root_index + 1 < len(parts):
|
||||
rel_parts = list(parts[detection_root_index + 1 :])
|
||||
else:
|
||||
rel_path = Path(path) if not Path(path).is_absolute() else Path(*parts[1:])
|
||||
rel_parts = list(rel_path.parts)
|
||||
|
||||
if label_path:
|
||||
image_path = image_root.joinpath(*rel_parts).with_suffix(".jpg")
|
||||
if image_path.exists():
|
||||
return image_path
|
||||
image_rel_parts = ["images" if part == "labels" else part for part in rel_parts]
|
||||
image_path = image_root.joinpath(*image_rel_parts).with_suffix(".jpg")
|
||||
if image_path.exists():
|
||||
return image_path
|
||||
for suffix in ("png", *sorted(IMG_FORMATS - {"jpg", "jpeg", "png"}), "jpeg"):
|
||||
candidate = image_path.with_suffix(f".{suffix}")
|
||||
if candidate.exists():
|
||||
return candidate
|
||||
return image_path
|
||||
|
||||
return image_root.joinpath(*rel_parts)
|
||||
|
||||
|
||||
def label_path_for_entry(entry: str) -> Path:
|
||||
suffix = Path(entry).suffix.lower().lstrip(".")
|
||||
if suffix == "txt":
|
||||
return Path(entry)
|
||||
return Path(img2label_path(entry))
|
||||
|
||||
|
||||
def parse_known_label_key(parts: list[str], class_map: dict[str, int]) -> tuple[float, ...] | None:
|
||||
if len(parts) < 7 or parts[0] not in class_map:
|
||||
return None
|
||||
coords = [float(x) for x in parts[1:5]]
|
||||
difficulty = float(parts[5]) + float(parts[6])
|
||||
row = np.array([class_map[parts[0]], *coords, difficulty], dtype=np.float32)
|
||||
return tuple(float(x) for x in row)
|
||||
|
||||
|
||||
def deduplicate_label_file(label_path: Path, class_map: dict[str, int]) -> tuple[int, list[str], list[str]]:
|
||||
"""Return duplicate count, cleaned raw lines, and warnings."""
|
||||
if not label_path.exists():
|
||||
return 0, [], []
|
||||
|
||||
raw_lines = read_lines(label_path)
|
||||
seen: OrderedDict[tuple[float, ...], int] = OrderedDict()
|
||||
keep = [True] * len(raw_lines)
|
||||
warnings = []
|
||||
|
||||
for idx, line in enumerate(raw_lines):
|
||||
stripped = line.strip()
|
||||
if not stripped:
|
||||
continue
|
||||
parts = stripped.split()
|
||||
try:
|
||||
key = parse_known_label_key(parts, class_map)
|
||||
except ValueError as exc:
|
||||
warnings.append(f"{label_path}:{idx + 1}: cannot parse label ({exc})")
|
||||
continue
|
||||
if key is None:
|
||||
continue
|
||||
if key in seen:
|
||||
keep[idx] = False
|
||||
else:
|
||||
seen[key] = idx
|
||||
|
||||
cleaned = [line for line, should_keep in zip(raw_lines, keep) if should_keep]
|
||||
return len(raw_lines) - len(cleaned), cleaned, warnings
|
||||
|
||||
|
||||
def exif_size(img: Image.Image) -> tuple[int, int]:
|
||||
size = img.size
|
||||
if img.format == "JPEG":
|
||||
try:
|
||||
if exif := img.getexif():
|
||||
if exif.get(274, None) in {6, 8}:
|
||||
size = size[1], size[0]
|
||||
except Exception:
|
||||
pass
|
||||
return size
|
||||
|
||||
|
||||
def image_corruption_reason(image_path: Path) -> str | None:
|
||||
try:
|
||||
with Image.open(image_path) as im:
|
||||
im.verify()
|
||||
shape = exif_size(im)
|
||||
if shape[0] <= 9 or shape[1] <= 9:
|
||||
return f"image size {shape} <10 pixels"
|
||||
if (im.format or "").lower() not in IMG_FORMATS:
|
||||
return f"invalid image format {im.format}"
|
||||
if image_path.suffix.lower() in {".jpg", ".jpeg"}:
|
||||
with image_path.open("rb") as f:
|
||||
f.seek(-2, 2)
|
||||
if f.read() != b"\xff\xd9":
|
||||
return "corrupt JPEG end marker"
|
||||
except Exception as exc:
|
||||
return str(exc)
|
||||
return None
|
||||
|
||||
|
||||
def unlink_if_exists(path: Path) -> bool:
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def progress_iter(items: list[str], desc: str, interval: int):
|
||||
total = len(items)
|
||||
if tqdm is not None:
|
||||
yield from tqdm(items, total=total, desc=desc, unit="entry")
|
||||
return
|
||||
|
||||
for idx, item in enumerate(items, 1):
|
||||
if interval > 0 and (idx == 1 or idx % interval == 0 or idx == total):
|
||||
print(f"{desc}: {idx}/{total}", flush=True)
|
||||
yield item
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
data_file = Path(args.data)
|
||||
data = load_ground_yaml(data_file)
|
||||
data_dir = data_file.parent
|
||||
image_root = Path(data["path"])
|
||||
class_map = data.get("class_map", {})
|
||||
if not class_map:
|
||||
raise SystemExit(f"{data_file} does not contain class_map; this cleaner is for ground 2D labels.")
|
||||
|
||||
update_lists = args.apply and not args.no_update_lists
|
||||
backup = not args.no_backup
|
||||
report_lines = []
|
||||
label_files_seen: set[Path] = set()
|
||||
stats = {
|
||||
"images": 0,
|
||||
"labels": 0,
|
||||
"duplicate_rows": 0,
|
||||
"labels_rewritten": 0,
|
||||
"corrupt_images": 0,
|
||||
"images_removed": 0,
|
||||
"labels_removed": 0,
|
||||
"list_entries_removed": 0,
|
||||
}
|
||||
|
||||
for split in args.splits:
|
||||
split_path = resolve_split_path(data.get(split), data_dir)
|
||||
if split_path is None:
|
||||
continue
|
||||
if not split_path.exists():
|
||||
report_lines.append(f"[missing split] {split}: {split_path}")
|
||||
continue
|
||||
|
||||
entries = split_entries(split_path)
|
||||
kept_entries = []
|
||||
removed_from_split = 0
|
||||
|
||||
for entry in progress_iter(entries, f"Scanning {split}", args.progress_interval):
|
||||
expanded_entry = expand_split_entry(entry, split_path)
|
||||
suffix = Path(expanded_entry).suffix.lower().lstrip(".")
|
||||
if suffix in IMG_FORMATS:
|
||||
image_path = ground_image_path_from_label_path(expanded_entry, image_root, label_path=False)
|
||||
label_path = label_path_for_entry(expanded_entry)
|
||||
elif suffix == "txt":
|
||||
label_path = Path(expanded_entry)
|
||||
image_path = ground_image_path_from_label_path(expanded_entry, image_root, label_path=True)
|
||||
else:
|
||||
report_lines.append(f"[unsupported entry] {split}: {entry}")
|
||||
kept_entries.append(entry)
|
||||
continue
|
||||
|
||||
stats["images"] += 1
|
||||
reason = image_corruption_reason(image_path)
|
||||
if reason:
|
||||
stats["corrupt_images"] += 1
|
||||
removed_from_split += 1
|
||||
report_lines.append(f"[corrupt] {image_path} | {reason}")
|
||||
report_lines.append(f" label: {label_path}")
|
||||
if args.apply:
|
||||
if unlink_if_exists(image_path):
|
||||
stats["images_removed"] += 1
|
||||
if unlink_if_exists(label_path):
|
||||
stats["labels_removed"] += 1
|
||||
continue
|
||||
|
||||
kept_entries.append(entry)
|
||||
if label_path in label_files_seen:
|
||||
continue
|
||||
label_files_seen.add(label_path)
|
||||
if label_path.exists():
|
||||
stats["labels"] += 1
|
||||
dup_count, cleaned_lines, warnings = deduplicate_label_file(label_path, class_map)
|
||||
report_lines.extend(f"[warning] {warning}" for warning in warnings)
|
||||
if dup_count:
|
||||
stats["duplicate_rows"] += dup_count
|
||||
report_lines.append(f"[duplicates] {label_path}: remove {dup_count}")
|
||||
if args.apply:
|
||||
write_lines(label_path, cleaned_lines, backup=backup)
|
||||
stats["labels_rewritten"] += 1
|
||||
|
||||
if update_lists and removed_from_split:
|
||||
write_lines(split_path, kept_entries, backup=backup)
|
||||
stats["list_entries_removed"] += removed_from_split
|
||||
report_lines.append(f"[list updated] {split_path}: removed {removed_from_split} corrupt entries")
|
||||
|
||||
mode = "APPLIED" if args.apply else "DRY RUN"
|
||||
summary = [
|
||||
f"Mode: {mode}",
|
||||
f"Images checked: {stats['images']}",
|
||||
f"Label files checked: {stats['labels']}",
|
||||
f"Duplicate label rows found: {stats['duplicate_rows']}",
|
||||
f"Label files rewritten: {stats['labels_rewritten']}",
|
||||
f"Corrupt images found: {stats['corrupt_images']}",
|
||||
f"Images removed: {stats['images_removed']}",
|
||||
f"Labels removed with corrupt images: {stats['labels_removed']}",
|
||||
f"Split list entries removed: {stats['list_entries_removed']}",
|
||||
]
|
||||
|
||||
output = "\n".join(summary + (["", *report_lines] if report_lines else []))
|
||||
print(output)
|
||||
if args.report:
|
||||
report_path = Path(args.report)
|
||||
report_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
report_path.write_text(output + "\n", encoding="utf-8")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
363
tools/convert_gt_to_label/convert_json_to_json.md
Executable file
363
tools/convert_gt_to_label/convert_json_to_json.md
Executable file
@@ -0,0 +1,363 @@
|
||||
# `convert_json_to_json.py` 说明文档
|
||||
|
||||
## 1. 脚本用途
|
||||
|
||||
`tools/convert_gt_to_label/convert_json_to_json.py` 的作用是:
|
||||
|
||||
- 读取原始 GT 标注 JSON;
|
||||
- 从 `asso_list` 中提取每个目标的 `camera_mea` 和可选的 `lidar_mea`;
|
||||
- 结合标定文件,将原始标注转换为项目当前使用的目标 JSON 格式;
|
||||
- 在输出中同时写入:
|
||||
- 2D 框信息;
|
||||
- 相机系 3D 信息;
|
||||
- ego 系 3D 信息;
|
||||
- 车辆前后左右面的附加信息(仅部分目标类型会生成)。
|
||||
|
||||
这个脚本本质上是一个“原始标注格式 -> 最终训练/评估使用 JSON 格式”的转换器,而不是简单的字段重命名脚本。
|
||||
|
||||
---
|
||||
|
||||
## 2. 输入与输出
|
||||
|
||||
### 2.1 单文件模式
|
||||
|
||||
命令格式:
|
||||
|
||||
```bash
|
||||
python3 tools/convert_gt_to_label/convert_json_to_json.py \
|
||||
input.json \
|
||||
output.json \
|
||||
--calib-path /path/to/case/calib
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `input.json`:原始标注文件。
|
||||
- `output.json`:转换后的输出文件。
|
||||
- `--calib-path`:标定目录。脚本会读取:
|
||||
|
||||
```text
|
||||
<calib-path>/L2_calib/camera4.json
|
||||
```
|
||||
|
||||
如果不传 `--calib-path`,代码默认使用 `input.json` 的父目录,但只有当该目录下确实存在 `L2_calib/camera4.json` 时才可正常工作。
|
||||
|
||||
### 2.2 批量模式
|
||||
|
||||
命令格式:
|
||||
|
||||
```bash
|
||||
python3 tools/convert_gt_to_label/convert_json_to_json.py \
|
||||
--json-root /path/to/dataset_root
|
||||
```
|
||||
|
||||
或使用封装脚本:
|
||||
|
||||
```bash
|
||||
bash tools/convert_gt_to_label/run_convert_json_to_json.sh /path/to/dataset_root
|
||||
```
|
||||
|
||||
批量模式下,脚本会递归查找:
|
||||
|
||||
```text
|
||||
<json-root>/**/annotations_20260320/*.json
|
||||
```
|
||||
|
||||
并假设每个 case 目录结构类似:
|
||||
|
||||
```text
|
||||
case_xxx/
|
||||
├── annotations_20260320/
|
||||
│ └── xxx.json
|
||||
├── calib/
|
||||
│ └── L2_calib/camera4.json
|
||||
└── labels_20260320_with_ego/
|
||||
└── xxx.json # 脚本输出
|
||||
```
|
||||
|
||||
注意:代码当前实际输出目录是 `labels_20260320_with_ego`。
|
||||
|
||||
---
|
||||
|
||||
## 3. 转换主流程
|
||||
|
||||
单个目标的大致处理链路如下:
|
||||
|
||||
1. 从原始标注的 `asso_list` 中逐个读取对象。
|
||||
2. 从 `camera_mea` 读取:
|
||||
- `cls`
|
||||
- 2D 框位置
|
||||
- `confidence`
|
||||
3. 用 `DETECTION_CLASS_MAP` 将原始类别名映射为项目内部类别 id。
|
||||
4. 若目标存在 `lidar_mea`,则进一步计算:
|
||||
- 相机系 3D 中心点、尺寸、朝向;
|
||||
- ego 系 3D 信息;
|
||||
- 部分目标的前/后/左/右面信息。
|
||||
5. 将最终结果组织成目标 JSON 条目,包含:
|
||||
- `type`
|
||||
- `type_name`
|
||||
- `box2d`
|
||||
- `3d_ori`
|
||||
- `3d_front/back/left/right`
|
||||
- `3d_ori_ego`
|
||||
- `3d_ori_ego_from_cam`
|
||||
- 一致性校验字段等。
|
||||
|
||||
---
|
||||
|
||||
## 4. `DETECTION_CLASS_MAP` 的作用
|
||||
|
||||
`DETECTION_CLASS_MAP` 是这个脚本里最关键的类别标准化入口。
|
||||
|
||||
代码位置:
|
||||
|
||||
- 定义:`tools/convert_gt_to_label/convert_json_to_json.py`
|
||||
- 使用:`build_label_row()` 中先取 `raw_label_name`,再执行 `label_det_cls = DETECTION_CLASS_MAP.get(raw_label_name, -1)`
|
||||
|
||||
它的职责不是单纯“把字符串换成数字”,而是同时决定了下面三件事:
|
||||
|
||||
### 4.1 决定原始类别如何映射到输出类别
|
||||
|
||||
当前代码会先取原始类别名:
|
||||
|
||||
- 若 `camera_mea["cls"] == "vehicle"`,则使用 `camera_mea["subcls"]`
|
||||
- 否则直接使用 `camera_mea["cls"]`
|
||||
|
||||
随后脚本会先通过 `DETECTION_CLASS_MAP` 把原始类别归一化成标准类别 id,再通过 `CLASS_MAP_MODEL` 得到标准类别名,最终写入输出中的:
|
||||
|
||||
- `type`:类别 id
|
||||
- `type_name`:标准类别名
|
||||
|
||||
例如:
|
||||
|
||||
- `tanker -> 6 -> truck`
|
||||
- `large_truck -> 6 -> truck`
|
||||
- `motorcyclist -> 10 -> bicyclist`
|
||||
- `tricyclist -> 12 -> tricycle`
|
||||
|
||||
这说明 `DETECTION_CLASS_MAP` 做的是“原始类别标准化”,不是“原样透传”。
|
||||
|
||||
### 4.2 决定目标是否被直接过滤
|
||||
|
||||
如果映射结果为 `-1`,目标会被直接丢弃,不写入输出。
|
||||
|
||||
当前脚本中:
|
||||
|
||||
- `carton -> -1`
|
||||
|
||||
对应逻辑在 `build_label_row()` 中:
|
||||
|
||||
- 若 `label_det_cls < 0`,函数直接返回 `None`
|
||||
- 上层 `generate_label_rows()` 会跳过该目标
|
||||
|
||||
也就是说,`DETECTION_CLASS_MAP` 同时承担了“保留/过滤白名单”的作用。
|
||||
|
||||
### 4.3 间接决定是否生成 3D 信息
|
||||
|
||||
在 `build_3d_label()` 中,存在如下规则:
|
||||
|
||||
- 若 `label_lidar is None`,不生成有效 3D;
|
||||
- 若类别 id 不在 `SUPPORTED_3D_CLASS_IDS` 中,不生成有效 3D;
|
||||
- 若类别 id 在 `COMPLETE_3D_CLASS_IDS` 中,只生成 `3d_ori`,不生成 `3d_front/back/left/right`;
|
||||
|
||||
因此,`DETECTION_CLASS_MAP` 还会影响 3D 处理分支。
|
||||
|
||||
结合当前类别定义,可理解为:
|
||||
|
||||
- `0~8`:`face_3d_classes`,可进入 3D 处理,且会额外尝试生成 `3d_front/back/left/right`
|
||||
- `9~12`:`complete_3d_classes`,可进入 3D 处理,但只保留 `3d_ori`
|
||||
- `13~16`:只保留 2D 或空 3D 占位
|
||||
- `-1`:直接过滤,不输出
|
||||
|
||||
所以,`DETECTION_CLASS_MAP` 不只是“类别映射表”,它还在事实上定义了哪些类别属于当前 3D 支持范围。
|
||||
|
||||
---
|
||||
|
||||
## 5. 当前 `DETECTION_CLASS_MAP` 解释
|
||||
|
||||
当前映射如下:
|
||||
|
||||
| 原始类别 / 子类 | 映射后 id | 输出标准类 | 3D 行为 |
|
||||
| --- | ---: | --- |
|
||||
| `car` | 0 | `car` | `face_3d` |
|
||||
| `suv` | 1 | `suv` | `face_3d` |
|
||||
| `pickup` | 2 | `pickup` | `face_3d` |
|
||||
| `medium_car` | 3 | `medium_car` | `face_3d` |
|
||||
| `van` | 4 | `van` | `face_3d` |
|
||||
| `bus` | 5 | `bus` | `face_3d` |
|
||||
| `truck` / `tanker` / `large_truck` / `construction_vehicle` | 6 | `truck` | `face_3d` |
|
||||
| `special_vehicle` | 7 | `special_vehicle` | `face_3d` |
|
||||
| `unknown` | 8 | `unknown` | `face_3d` |
|
||||
| `pedestrian` | 9 | `pedestrian` | `complete_3d` |
|
||||
| `bicyclist` / `motorcyclist` | 10 | `bicyclist` | `complete_3d` |
|
||||
| `bicycle` / `motorcycle` | 11 | `bicycle` | `complete_3d` |
|
||||
| `tricycle` / `tricyclist` | 12 | `tricycle` | `complete_3d` |
|
||||
| `traffic_sign` | 13 | `traffic_sign` | 仅 2D |
|
||||
| `wheel` | 14 | `wheel` | 仅 2D |
|
||||
| `plate` | 15 | `plate` | 仅 2D |
|
||||
| `face` | 16 | `face` | 仅 2D |
|
||||
|
||||
可以看出,这张表体现了三类策略:
|
||||
|
||||
### 5.1 一对一映射
|
||||
|
||||
例如:
|
||||
|
||||
- `pedestrian -> 9`
|
||||
- `wheel -> 14`
|
||||
|
||||
适用于原始类别与项目标准类别完全一致的情况。
|
||||
|
||||
### 5.2 多对一归并
|
||||
|
||||
例如:
|
||||
|
||||
- `tanker`
|
||||
- `large_truck`
|
||||
- `construction_vehicle`
|
||||
|
||||
都被归并为 `truck(6)`。
|
||||
|
||||
这意味着训练/评估侧不再区分这些细粒度障碍物,而是把它们当成同一大类处理。
|
||||
|
||||
### 5.3 显式过滤
|
||||
|
||||
例如:
|
||||
|
||||
- `carton -> -1`
|
||||
|
||||
说明该类别虽然可能出现在原始标注中,但当前转换目标并不希望保留它。
|
||||
|
||||
---
|
||||
|
||||
## 6. `DETECTION_CLASS_MAP` 的维护原则
|
||||
|
||||
后续如果原始 GT 中新增了 `camera_mea.cls` 类别,最先需要检查的就是这张表。
|
||||
|
||||
### 6.1 如果原始类别未出现在映射表中
|
||||
|
||||
当前代码会直接执行:
|
||||
|
||||
```python
|
||||
label_det_cls = DETECTION_CLASS_MAP.get(raw_label_name, -1)
|
||||
```
|
||||
|
||||
这意味着:
|
||||
|
||||
- 未知类别会被映射为 `-1`;
|
||||
- `build_label_row()` 会直接返回 `None`,该目标不会写入输出。
|
||||
|
||||
因此,只要数据源里出现新类别,就必须同步更新 `DETECTION_CLASS_MAP`,否则会被静默过滤。
|
||||
|
||||
### 6.2 建议先用统计脚本盘点类别
|
||||
|
||||
同目录已经提供:
|
||||
|
||||
```text
|
||||
tools/convert_gt_to_label/stat_gt_categories.py
|
||||
```
|
||||
|
||||
它可以统计原始 GT 中所有 `camera_mea.cls` 的取值,并生成映射模板,适合在更新 `DETECTION_CLASS_MAP` 前先做盘点。
|
||||
|
||||
建议流程:
|
||||
|
||||
1. 先运行 `stat_gt_categories.py` 统计现网类别;
|
||||
2. 确认每个原始类别应该:
|
||||
- 映射到哪个标准类;
|
||||
- 还是直接过滤;
|
||||
3. 再更新 `DETECTION_CLASS_MAP`;
|
||||
4. 最后运行 `convert_json_to_json.py` 做验证。
|
||||
|
||||
### 6.3 修改映射时要同步关注 3D 语义
|
||||
|
||||
例如把某个新类别映射到:
|
||||
|
||||
- `0~8`:表示该类别会进入 `face_3d` 分支;
|
||||
- `9~12`:表示该类别会进入 `complete_3d` 分支;
|
||||
- `13~16`:表示该类别只保留 2D 或空 3D 占位;
|
||||
- `-1`:表示完全过滤。
|
||||
|
||||
所以改这张表时,不能只从“类别名是否合理”来判断,还要同时确认:
|
||||
|
||||
- 是否希望输出 3D;
|
||||
- 是否希望参与车辆面信息计算;
|
||||
- 是否会影响下游训练/评估类别定义。
|
||||
|
||||
---
|
||||
|
||||
## 7. 输出类别与 `CLASS_MAP` 的关系
|
||||
|
||||
脚本中还有一张表:
|
||||
|
||||
```python
|
||||
CLASS_MAP = {
|
||||
"car": 0,
|
||||
"suv": 1,
|
||||
"pickup": 2,
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
它和 `DETECTION_CLASS_MAP` 的关系可以理解为:
|
||||
|
||||
- `DETECTION_CLASS_MAP`:原始类别名 / 原始 `subcls` -> 内部标准 id
|
||||
- `CLASS_MAP_MODEL`:内部标准 id -> 规范化后的标准类别名
|
||||
- `CLASS_MAP`:标准类别名 -> 输出类别 id
|
||||
|
||||
两者配合后,最终输出对象里会写入:
|
||||
|
||||
- `type`: 例如 `6`
|
||||
- `type_name`: 例如 `truck`
|
||||
|
||||
---
|
||||
|
||||
## 8. 使用这个脚本时最容易踩的点
|
||||
|
||||
### 8.1 类别未配置会被直接过滤
|
||||
|
||||
如果原始类别或车辆 `subcls` 出现新值,但 `DETECTION_CLASS_MAP` 没更新,该目标会被跳过,不会写入输出。
|
||||
|
||||
### 8.2 批量模式只扫描固定目录名
|
||||
|
||||
批量模式只查找:
|
||||
|
||||
```text
|
||||
annotations/*.json
|
||||
```
|
||||
|
||||
如果目录名变化,需要修改 `find_annotation_files()`。
|
||||
|
||||
### 8.3 批量模式输出目录是 `labels_json`
|
||||
|
||||
批量模式当前实际写入的是:
|
||||
|
||||
```text
|
||||
labels_json
|
||||
```
|
||||
|
||||
使用时应以代码实际行为为准。
|
||||
|
||||
### 8.4 并非所有类别都会生成有效 3D
|
||||
|
||||
即使原始数据里存在 `lidar_mea`,也只有:
|
||||
|
||||
- `0~8` 类会生成 `face_3d`
|
||||
- `9~12` 类会生成 `complete_3d`
|
||||
- `13~16` 类会写成空 3D
|
||||
|
||||
### 8.5 输出类别名是归一化后的标准类名
|
||||
|
||||
例如:
|
||||
|
||||
- `tanker` 最终输出为 `truck`
|
||||
- `motorcyclist` 最终输出为 `bicyclist`
|
||||
- `tricyclist` 最终输出为 `tricycle`
|
||||
|
||||
---
|
||||
|
||||
## 9. 一句话总结
|
||||
|
||||
如果只抓一个重点来理解这个脚本,那么就是:
|
||||
|
||||
`DETECTION_CLASS_MAP` 是原始 GT 到项目标准标签体系的总入口,它同时决定了“类别如何归一化成标准输出、目标是否过滤、以及目标进入哪种 3D 分支”。
|
||||
理解这张表,基本就理解了这个脚本一半以上的业务语义。
|
||||
1408
tools/convert_gt_to_label/convert_json_to_json.py
Executable file
1408
tools/convert_gt_to_label/convert_json_to_json.py
Executable file
File diff suppressed because it is too large
Load Diff
1397
tools/convert_gt_to_label/convert_json_to_txt.py
Executable file
1397
tools/convert_gt_to_label/convert_json_to_txt.py
Executable file
File diff suppressed because it is too large
Load Diff
233
tools/convert_gt_to_label/convert_results_to_example_format.py
Executable file
233
tools/convert_gt_to_label/convert_results_to_example_format.py
Executable file
@@ -0,0 +1,233 @@
|
||||
#!/usr/bin/env python3
|
||||
# coding: utf-8
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from uuid import NAMESPACE_URL, uuid5
|
||||
|
||||
|
||||
DEFAULT_RESULTS_DIR = "/data1/dongying/Mono3d/D4Q2/feishu_project/0416_chenyile/results"
|
||||
DEFAULT_EXAMPLE_JSON = "/data1/dongying/Mono3d/D4Q2/feishu_project/0416_dongying/example_format.json"
|
||||
DEFAULT_VIS_DIR = "/data1/dongying/Mono3d/D4Q2/feishu_project/0416_chenyile/vis"
|
||||
DEFAULT_OUTPUT_JSON = "/data1/dongying/Mono3d/D4Q2/feishu_project/0416_chenyile/results_example_format.json"
|
||||
|
||||
DEFAULT_USER_ID = 1698
|
||||
DEFAULT_SOURCE = "default"
|
||||
DEFAULT_IMAGE_SIZE = [0, 0]
|
||||
IMAGE_SUFFIXES = (".jpg", ".jpeg", ".png", ".bmp", ".webp")
|
||||
|
||||
LABEL_OVERRIDES = {
|
||||
"pedestrian": (["pedestrian", "pedestrian"], ["行人", "行人"]),
|
||||
"face": (["sensitive_area", "face"], ["敏感区域", "人脸"]),
|
||||
"suv": (["vehicle", "suv"], ["机动车", "SUV"]),
|
||||
"car": (["vehicle", "car"], ["机动车", "小轿车"]),
|
||||
"truck": (["vehicle", "truck"], ["机动车", "卡车"]),
|
||||
"traffic_sign": (["traffic_sign", "traffic_sign_unknown"], ["交通标志", "未知交通标志"]),
|
||||
"plate": (["sensitive_area", "plate"], ["敏感区域", "车牌"]),
|
||||
"wheel": (["vehicle", "wheel"], ["机动车", "车轮"]),
|
||||
"traffic_cone": (["road_barrier", "traffic_cone"], ["路障", "交通锥"]),
|
||||
}
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="将结果目录重排为 example_format.json 同款结构。")
|
||||
parser.add_argument("results_dir", nargs="?", default=DEFAULT_RESULTS_DIR, help="输入结果目录。")
|
||||
parser.add_argument("output_json", nargs="?", default=DEFAULT_OUTPUT_JSON, help="输出 JSON 文件。")
|
||||
parser.add_argument("--example-json", default=DEFAULT_EXAMPLE_JSON, help="示例 JSON,用于补充已知标签映射。")
|
||||
parser.add_argument("--vis-dir", default=DEFAULT_VIS_DIR, help="可选图片目录,用于补 image_name 和 image_size。")
|
||||
parser.add_argument("--userid", type=int, default=DEFAULT_USER_ID, help="输出 annotations.userid。")
|
||||
parser.add_argument("--source", default=DEFAULT_SOURCE, help="输出 annotations.source。")
|
||||
parser.add_argument(
|
||||
"--unknown-occlusion",
|
||||
default="no",
|
||||
choices=["no", "slight", "partial", "heavy"],
|
||||
help="结果里 unknown_occlusion 的回填值。",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def load_example_label_map(example_json: Path) -> dict[str, tuple[list[str], list[str]]]:
|
||||
if not example_json.is_file():
|
||||
return {}
|
||||
|
||||
with example_json.open("r", encoding="utf-8") as file_obj:
|
||||
records = json.load(file_obj)
|
||||
|
||||
label_map: dict[str, tuple[list[str], list[str]]] = {}
|
||||
for record in records:
|
||||
for annotation_block in record.get("annotations", []):
|
||||
for annotation in annotation_block.get("anno", []):
|
||||
label = annotation.get("label") or []
|
||||
label_desc = annotation.get("labelDesc") or []
|
||||
if len(label) < 2 or len(label_desc) < 2:
|
||||
continue
|
||||
child_label = str(label[1])
|
||||
label_map[child_label] = ([str(item) for item in label], [str(item) for item in label_desc])
|
||||
return label_map
|
||||
|
||||
|
||||
def resolve_label_mapping(name: str, example_label_map: dict[str, tuple[list[str], list[str]]]) -> tuple[list[str], list[str]]:
|
||||
if name in LABEL_OVERRIDES:
|
||||
return LABEL_OVERRIDES[name]
|
||||
if name in example_label_map:
|
||||
return example_label_map[name]
|
||||
return (["vehicle", "unknown"], ["机动车", "未知车辆"])
|
||||
|
||||
|
||||
def normalize_occlusion(raw_occlusion: Any, unknown_occlusion: str) -> str:
|
||||
mapping = {
|
||||
"no_occlusion": "no",
|
||||
"slight_occlusion": "slight",
|
||||
"partial_occlusion": "partial",
|
||||
"heavy_occlusion": "heavy",
|
||||
"unknown_occlusion": unknown_occlusion,
|
||||
None: "no",
|
||||
}
|
||||
return mapping.get(raw_occlusion, unknown_occlusion)
|
||||
|
||||
|
||||
def stable_uuid(prefix: str, value: str) -> str:
|
||||
return str(uuid5(NAMESPACE_URL, f"{prefix}:{value}"))
|
||||
|
||||
|
||||
def build_vis_image_map(vis_dir: Path) -> dict[str, Path]:
|
||||
if not vis_dir.is_dir():
|
||||
raise FileNotFoundError(f"vis directory not found: {vis_dir}")
|
||||
|
||||
vis_image_map: dict[str, Path] = {}
|
||||
for image_path in sorted(vis_dir.rglob("*")):
|
||||
if not image_path.is_file() or image_path.suffix.lower() not in IMAGE_SUFFIXES:
|
||||
continue
|
||||
vis_image_map.setdefault(image_path.stem, image_path)
|
||||
return vis_image_map
|
||||
|
||||
|
||||
def detect_image_info(stem: str, vis_image_map: dict[str, Path]) -> tuple[str, list[int]]:
|
||||
image_path = vis_image_map.get(stem)
|
||||
if image_path is None:
|
||||
raise FileNotFoundError(f"vis image not found for result frame: {stem}")
|
||||
return image_path.name, list(DEFAULT_IMAGE_SIZE)
|
||||
|
||||
|
||||
def convert_detection(
|
||||
detection: dict[str, Any],
|
||||
stem: str,
|
||||
index: int,
|
||||
example_label_map: dict[str, tuple[list[str], list[str]]],
|
||||
unknown_occlusion: str,
|
||||
) -> dict[str, Any]:
|
||||
bbox_xyxy = detection.get("bbox_xyxy")
|
||||
if not isinstance(bbox_xyxy, list) or len(bbox_xyxy) != 4:
|
||||
raise ValueError(f"invalid bbox_xyxy in {stem}: {detection}")
|
||||
|
||||
label, label_desc = resolve_label_mapping(str(detection.get("name", "unknown")), example_label_map)
|
||||
fake_cls_id = detection.get("fake_cls_id", -1)
|
||||
annotation_attr = {
|
||||
"is_fake": "true" if fake_cls_id not in (-1, None) else "false",
|
||||
"is_inferred": "false",
|
||||
"occlusion": normalize_occlusion(detection.get("occlusion"), unknown_occlusion),
|
||||
"truncation": "no",
|
||||
}
|
||||
|
||||
return {
|
||||
"attr": annotation_attr,
|
||||
"data": {
|
||||
"point2d": [
|
||||
[float(bbox_xyxy[0]), float(bbox_xyxy[1])],
|
||||
[float(bbox_xyxy[2]), float(bbox_xyxy[3])],
|
||||
]
|
||||
},
|
||||
"id": index,
|
||||
"index": stable_uuid("annotation-index", f"{stem}:{index}"),
|
||||
"label": label,
|
||||
"labelDesc": label_desc,
|
||||
"type": "rect",
|
||||
}
|
||||
|
||||
|
||||
def convert_result_file(
|
||||
result_path: Path,
|
||||
example_label_map: dict[str, tuple[list[str], list[str]]],
|
||||
vis_image_map: dict[str, Path],
|
||||
userid: int,
|
||||
source: str,
|
||||
unknown_occlusion: str,
|
||||
) -> dict[str, Any]:
|
||||
with result_path.open("r", encoding="utf-8") as file_obj:
|
||||
detections = json.load(file_obj)
|
||||
|
||||
if not isinstance(detections, list):
|
||||
raise ValueError(f"result file must be a list: {result_path}")
|
||||
|
||||
stem = result_path.stem
|
||||
image_name, image_size = detect_image_info(stem, vis_image_map)
|
||||
anno = [
|
||||
convert_detection(detection, stem, index, example_label_map, unknown_occlusion)
|
||||
for index, detection in enumerate(detections)
|
||||
]
|
||||
|
||||
return {
|
||||
"annotations": [
|
||||
{
|
||||
"anno": anno,
|
||||
"anno_uuid": stable_uuid("anno", stem),
|
||||
"source": source,
|
||||
"userid": userid,
|
||||
}
|
||||
],
|
||||
"image_attr": None,
|
||||
"image_name": image_name,
|
||||
"image_size": image_size,
|
||||
"image_uuid": stable_uuid("image", stem),
|
||||
"package_image_uuid": stable_uuid("package-image", stem),
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
results_dir = Path(args.results_dir).resolve()
|
||||
output_json = Path(args.output_json).resolve()
|
||||
example_json = Path(args.example_json).resolve()
|
||||
vis_dir = Path(args.vis_dir).resolve()
|
||||
|
||||
if not results_dir.is_dir():
|
||||
raise FileNotFoundError(f"results directory not found: {results_dir}")
|
||||
|
||||
example_label_map = load_example_label_map(example_json)
|
||||
vis_image_map = build_vis_image_map(vis_dir)
|
||||
result_files = sorted(results_dir.glob("*.json"))
|
||||
if not result_files:
|
||||
raise ValueError(f"no json files found under {results_dir}")
|
||||
|
||||
selected_result_files = [result_path for result_path in result_files if result_path.stem in vis_image_map]
|
||||
skipped_result_files = [result_path for result_path in result_files if result_path.stem not in vis_image_map]
|
||||
if not selected_result_files:
|
||||
raise ValueError(f"no result frames matched vis images under {vis_dir}")
|
||||
|
||||
converted = [
|
||||
convert_result_file(
|
||||
result_path,
|
||||
example_label_map,
|
||||
vis_image_map,
|
||||
userid=args.userid,
|
||||
source=args.source,
|
||||
unknown_occlusion=args.unknown_occlusion,
|
||||
)
|
||||
for result_path in selected_result_files
|
||||
]
|
||||
|
||||
output_json.parent.mkdir(parents=True, exist_ok=True)
|
||||
with output_json.open("w", encoding="utf-8") as file_obj:
|
||||
json.dump(converted, file_obj, ensure_ascii=False, indent=2)
|
||||
|
||||
print(
|
||||
f"result_files={len(result_files)} matched_vis_files={len(selected_result_files)} "
|
||||
f"skipped_files={len(skipped_result_files)} output={output_json}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
1243
tools/convert_gt_to_label/converter.py
Executable file
1243
tools/convert_gt_to_label/converter.py
Executable file
File diff suppressed because it is too large
Load Diff
277
tools/convert_gt_to_label/count_visualization_batches.py
Executable file
277
tools/convert_gt_to_label/count_visualization_batches.py
Executable file
@@ -0,0 +1,277 @@
|
||||
#!/usr/bin/env python3
|
||||
# coding: utf-8
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
DEFAULT_SHOWPATH_ROOT = "/data1/dongying/Mono3d/D4Q2/data_visualization_for_check_camera2"
|
||||
DEFAULT_BATCH_GLOB = "batch_*"
|
||||
DEFAULT_IMAGE_EXTS = ".jpg,.jpeg,.png"
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="统计可视化结果根目录下每个 batch 的 2D/3D 图像数量。"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--showpath-root",
|
||||
default=DEFAULT_SHOWPATH_ROOT,
|
||||
help="可视化结果根目录,目录下应包含 batch_* 子目录。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--batch-glob",
|
||||
default=DEFAULT_BATCH_GLOB,
|
||||
help="batch 目录匹配模式,默认 batch_*。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--image-exts",
|
||||
default=DEFAULT_IMAGE_EXTS,
|
||||
help="参与统计的图片扩展名,逗号分隔,默认 .jpg,.jpeg,.png。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--check-pairs",
|
||||
action="store_true",
|
||||
help="额外校验 2D/3D 下的相对图片路径是否一一对应。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sample-mismatch-limit",
|
||||
type=int,
|
||||
default=10,
|
||||
help="配对校验失败时,最多展示多少条 only_in_2d/only_in_3d 样例。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-json",
|
||||
default=None,
|
||||
help="可选,将统计结果写入 JSON 文件。",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def normalize_image_exts(raw_exts):
|
||||
image_exts = []
|
||||
for ext in raw_exts.split(","):
|
||||
ext = ext.strip().lower()
|
||||
if not ext:
|
||||
continue
|
||||
if not ext.startswith("."):
|
||||
ext = f".{ext}"
|
||||
image_exts.append(ext)
|
||||
if not image_exts:
|
||||
raise ValueError("image-exts 不能为空。")
|
||||
return tuple(sorted(set(image_exts)))
|
||||
|
||||
|
||||
def collect_image_stats(image_dir, image_exts, collect_relpaths=False):
|
||||
count = 0
|
||||
relpaths = set() if collect_relpaths else None
|
||||
|
||||
if not image_dir.is_dir():
|
||||
return count, relpaths
|
||||
|
||||
for current_root, _, filenames in os.walk(image_dir):
|
||||
current_root_path = Path(current_root)
|
||||
for filename in filenames:
|
||||
if Path(filename).suffix.lower() not in image_exts:
|
||||
continue
|
||||
count += 1
|
||||
if relpaths is not None:
|
||||
file_path = current_root_path / filename
|
||||
relpaths.add(file_path.relative_to(image_dir).as_posix())
|
||||
|
||||
return count, relpaths
|
||||
|
||||
|
||||
def build_status(batch_summary, check_pairs):
|
||||
issues = []
|
||||
if not batch_summary["has_2d_dir"]:
|
||||
issues.append("MISSING_2D")
|
||||
if not batch_summary["has_3d_dir"]:
|
||||
issues.append("MISSING_3D")
|
||||
if batch_summary["count_diff"] != 0:
|
||||
issues.append("COUNT_DIFF")
|
||||
if check_pairs and (
|
||||
batch_summary["only_in_2d_count"] > 0 or batch_summary["only_in_3d_count"] > 0
|
||||
):
|
||||
issues.append("PAIR_DIFF")
|
||||
return "OK" if not issues else "+".join(issues)
|
||||
|
||||
|
||||
def analyze_batch(batch_dir, image_exts, check_pairs=False, sample_mismatch_limit=10):
|
||||
dir_2d = batch_dir / "2D"
|
||||
dir_3d = batch_dir / "3D"
|
||||
|
||||
count_2d, relpaths_2d = collect_image_stats(
|
||||
dir_2d, image_exts, collect_relpaths=check_pairs
|
||||
)
|
||||
count_3d, relpaths_3d = collect_image_stats(
|
||||
dir_3d, image_exts, collect_relpaths=check_pairs
|
||||
)
|
||||
|
||||
summary = {
|
||||
"batch_name": batch_dir.name,
|
||||
"batch_dir": str(batch_dir),
|
||||
"has_2d_dir": dir_2d.is_dir(),
|
||||
"has_3d_dir": dir_3d.is_dir(),
|
||||
"count_2d": count_2d,
|
||||
"count_3d": count_3d,
|
||||
"count_diff": count_2d - count_3d,
|
||||
}
|
||||
|
||||
if check_pairs:
|
||||
only_in_2d = sorted(relpaths_2d - relpaths_3d)
|
||||
only_in_3d = sorted(relpaths_3d - relpaths_2d)
|
||||
summary.update(
|
||||
{
|
||||
"only_in_2d_count": len(only_in_2d),
|
||||
"only_in_3d_count": len(only_in_3d),
|
||||
"only_in_2d_samples": only_in_2d[:sample_mismatch_limit],
|
||||
"only_in_3d_samples": only_in_3d[:sample_mismatch_limit],
|
||||
}
|
||||
)
|
||||
|
||||
summary["status"] = build_status(summary, check_pairs)
|
||||
return summary
|
||||
|
||||
|
||||
def build_total_row(batch_summaries, check_pairs):
|
||||
total_row = {
|
||||
"batch_name": "TOTAL",
|
||||
"count_2d": sum(item["count_2d"] for item in batch_summaries),
|
||||
"count_3d": sum(item["count_3d"] for item in batch_summaries),
|
||||
}
|
||||
total_row["count_diff"] = total_row["count_2d"] - total_row["count_3d"]
|
||||
|
||||
if check_pairs:
|
||||
total_row["only_in_2d_count"] = sum(
|
||||
item["only_in_2d_count"] for item in batch_summaries
|
||||
)
|
||||
total_row["only_in_3d_count"] = sum(
|
||||
item["only_in_3d_count"] for item in batch_summaries
|
||||
)
|
||||
|
||||
total_row["status"] = "OK"
|
||||
if any(item["status"] != "OK" for item in batch_summaries):
|
||||
total_row["status"] = "HAS_ISSUES"
|
||||
|
||||
return total_row
|
||||
|
||||
|
||||
def print_summary_table(batch_summaries, total_row, check_pairs):
|
||||
columns = [
|
||||
("batch_name", "batch"),
|
||||
("count_2d", "2D"),
|
||||
("count_3d", "3D"),
|
||||
("count_diff", "diff"),
|
||||
]
|
||||
if check_pairs:
|
||||
columns.extend(
|
||||
[
|
||||
("only_in_2d_count", "only_2d"),
|
||||
("only_in_3d_count", "only_3d"),
|
||||
]
|
||||
)
|
||||
columns.append(("status", "status"))
|
||||
|
||||
table_rows = batch_summaries + [total_row]
|
||||
widths = {}
|
||||
for key, title in columns:
|
||||
widths[key] = max(
|
||||
len(title),
|
||||
max(len(str(row.get(key, ""))) for row in table_rows),
|
||||
)
|
||||
|
||||
header = " ".join(title.ljust(widths[key]) for key, title in columns)
|
||||
separator = " ".join("-" * widths[key] for key, _ in columns)
|
||||
print(header)
|
||||
print(separator)
|
||||
for row in table_rows:
|
||||
print(
|
||||
" ".join(str(row.get(key, "")).ljust(widths[key]) for key, _ in columns)
|
||||
)
|
||||
|
||||
|
||||
def build_report(args, batch_summaries, total_row, image_exts):
|
||||
return {
|
||||
"generated_at": datetime.now().isoformat(timespec="seconds"),
|
||||
"showpath_root": str(Path(args.showpath_root).resolve()),
|
||||
"batch_glob": args.batch_glob,
|
||||
"image_exts": list(image_exts),
|
||||
"check_pairs": args.check_pairs,
|
||||
"sample_mismatch_limit": args.sample_mismatch_limit,
|
||||
"total_batches": len(batch_summaries),
|
||||
"total_summary": total_row,
|
||||
"batches": batch_summaries,
|
||||
}
|
||||
|
||||
|
||||
def print_pair_mismatch_details(batch_summaries):
|
||||
mismatched_batches = [
|
||||
item
|
||||
for item in batch_summaries
|
||||
if item["only_in_2d_count"] > 0 or item["only_in_3d_count"] > 0
|
||||
]
|
||||
if not mismatched_batches:
|
||||
return
|
||||
|
||||
print("\nPair mismatch details:")
|
||||
for item in mismatched_batches:
|
||||
print(
|
||||
f"- {item['batch_name']}: only_in_2d={item['only_in_2d_count']}, "
|
||||
f"only_in_3d={item['only_in_3d_count']}"
|
||||
)
|
||||
if item["only_in_2d_samples"]:
|
||||
print(f" only_in_2d samples: {item['only_in_2d_samples']}")
|
||||
if item["only_in_3d_samples"]:
|
||||
print(f" only_in_3d samples: {item['only_in_3d_samples']}")
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
image_exts = normalize_image_exts(args.image_exts)
|
||||
|
||||
showpath_root = Path(args.showpath_root)
|
||||
if not showpath_root.is_dir():
|
||||
print(f"可视化结果根目录不存在: {showpath_root}")
|
||||
return 1
|
||||
|
||||
batch_dirs = sorted(
|
||||
path for path in showpath_root.glob(args.batch_glob) if path.is_dir()
|
||||
)
|
||||
if not batch_dirs:
|
||||
print(
|
||||
f"未找到 batch 目录,root={showpath_root}, batch_glob={args.batch_glob}"
|
||||
)
|
||||
return 1
|
||||
|
||||
batch_summaries = [
|
||||
analyze_batch(
|
||||
batch_dir,
|
||||
image_exts,
|
||||
check_pairs=args.check_pairs,
|
||||
sample_mismatch_limit=args.sample_mismatch_limit,
|
||||
)
|
||||
for batch_dir in batch_dirs
|
||||
]
|
||||
total_row = build_total_row(batch_summaries, args.check_pairs)
|
||||
|
||||
print_summary_table(batch_summaries, total_row, args.check_pairs)
|
||||
if args.check_pairs:
|
||||
print_pair_mismatch_details(batch_summaries)
|
||||
|
||||
report = build_report(args, batch_summaries, total_row, image_exts)
|
||||
if args.output_json:
|
||||
output_json = Path(args.output_json)
|
||||
output_json.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(output_json, "w", encoding="utf-8") as f:
|
||||
json.dump(report, f, ensure_ascii=False, indent=2)
|
||||
print(f"\n统计结果已写入: {output_json}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
1610
tools/convert_gt_to_label/example.json
Executable file
1610
tools/convert_gt_to_label/example.json
Executable file
File diff suppressed because it is too large
Load Diff
4028
tools/convert_gt_to_label/example_converted.json
Executable file
4028
tools/convert_gt_to_label/example_converted.json
Executable file
File diff suppressed because it is too large
Load Diff
61
tools/convert_gt_to_label/example_converted.txt
Executable file
61
tools/convert_gt_to_label/example_converted.txt
Executable file
@@ -0,0 +1,61 @@
|
||||
0 0.4478526041666667 0.5342865740740741 0.021103125000000014 0.032756481481481435 -5.322678325758643 0.9269782749746722 62.31183979627242 4.2753500939967495 1.5497414206240265 1.8667275547695945 -1.6644804990396993 0.4484375 0.5361111111111111 0.44895833333333335 0.5527777777777778 -1.5792673365929495 2 -5.5226518245574 0.9269782749746723 64.4401408103063 -1.5789873571664765 0.4484375 0.5351851851851852 0.0 1.0 -5.122704826959886 0.9269782749746723 60.18353878223853 -1.5795671329741405 0.44895833333333335 0.5370370370370371 0.9972042575955438 1.0 -6.251949159817303 0.9269782749746722 62.22452623545889 -1.5643425280922993 0.4375 0.5361111111111111 0.0 -1.0 -4.393407491699982 0.9269782749746723 62.39915335708595 -1.5941883724040449 0.45989583333333334 0.5361111111111111 0.0 -1.0 0.96
|
||||
0 0.4192578125 0.5268273148148148 0.016055208333333345 0.028360185185185204 -11.178087914511165 1.126623659839888 91.10809174378281 4.3394085453321996 1.638317293102368 1.8966120615775184 -1.6809316457528798 0.42135416666666664 0.5324074074074074 0.42135416666666664 0.5444444444444444 -1.5588513648773974 2 -11.416566188320822 1.126623659839888 93.26465028547517 -1.5591271864689538 0.42135416666666664 0.5324074074074074 0.0 1.0 -10.939609640701509 1.126623659839888 88.95153320209045 -1.5585621891865657 0.42083333333333334 0.5333333333333333 0.9960981407977217 1.0 -12.120648380746003 1.126623659839888 91.00386077179513 -1.5485226283513929 0.41354166666666664 0.5324074074074074 0.0 -1.0 -10.235527448276326 1.126623659839888 91.21232271577048 -1.569182650706946 0.42864583333333334 0.5324074074074074 0.03205149144398053 -1.0 0.96
|
||||
0 0.4901458333333333 0.5398986111111111 0.03416041666666665 0.056117592592592566 -1.2207727474111165 0.7417515610941243 40.95386615446082 4.255067102022341 1.602117779670393 1.9317511054345955 -1.6031243112161284 0.490625 0.5407407407407407 0.490625 0.5675925925925925 -1.573324649211065 2 -1.2895396394130736 0.7417515610941243 43.08028806114882 -1.5731998432485674 0.490625 0.5398148148148149 0.0 1.0 -1.1520058554091595 0.7417515610941243 38.827444247772824 -1.5734631264946095 0.490625 0.5416666666666666 0.9991507706959047 1.0 -2.1861436265007925 0.7417515610941243 40.92264678318958 -1.5497536786784258 0.47291666666666665 0.5407407407407407 0.0 -1.0 -0.25540186832144046 0.7417515610941243 40.98508552573206 -1.5968928111680094 0.5083333333333333 0.5407407407407407 0.0 -1.0 0.95
|
||||
0 0.5539872395833334 0.5398157407407407 0.04805468749999999 0.06785370370370374 1.7965333771789131 0.6810791278357566 35.80506950417485 4.364611388811167 1.6452826101350673 1.9515451783566093 -1.6303546516568754 0.5515625 0.5416666666666666 0.5515625 0.5731481481481482 -1.6804879998811413 2 1.6666357328349335 0.6810791278357566 37.98350581167363 -1.674204405842794 0.546875 0.5407407407407407 0.0 1.0 1.9264310215228928 0.6810791278357566 33.626633196676075 -1.6875809468832172 0.5567708333333333 0.5435185185185185 0.9621084147554989 1.0 0.8224909038548001 0.6810791278357566 35.746988474979624 -1.6533592694725814 0.5307291666666667 0.5416666666666666 0.22926229906695217 -1.0 2.7705758505030262 0.6810791278357566 35.86315053337008 -1.7074556224660682 0.571875 0.5416666666666666 0.0 -1.0 0.94
|
||||
0 0.43622265624999995 0.5246351851851851 0.007328645833333347 0.01318333333333328 -1 0.94
|
||||
0 0.4685973958333333 0.527699074074074 0.007571875000000006 0.018261111111111093 -1 0.93
|
||||
0 0.52715078125 0.5331833333333333 0.02214843750000005 0.04639814814814816 1.2888118094408063 0.7312509380193262 52.36979611561394 4.493534510282726 1.6635234267603494 1.8789202854762639 -1.647023413387698 0.5322916666666667 0.5351851851851852 0.5322916666666667 0.5564814814814815 -1.6716282774998268 2 1.1177130965291244 0.7312509380193262 54.61003903307075 -1.6674877288067362 0.5291666666666667 0.5342592592592592 0.0 1.0 1.4599105223524882 0.7312509380193262 50.129553198157126 -1.6761379357716946 0.5354166666666667 0.5361111111111111 0.9658868716740873 1.0 0.35207974420975546 0.7312509380193262 52.29825313719172 -1.6537554628208084 0.5182291666666666 0.5351851851851852 0.22981171922386145 -1.0 2.225543874671857 0.7312509380193262 52.44133909403615 -1.6894366936903278 0.5458333333333333 0.5351851851851852 0.0 -1.0 0.92
|
||||
0 0.10260625 0.49172916666666666 0.06731458333333332 0.0626805555555555 -28.223506943782933 -0.5737357863269261 32.63411845801956 11.245817250335156 3.34837425993108 2.9292775402711784 1.508036394208803 0.09895833333333333 0.5009259259259259 0.09947916666666666 0.5444444444444444 2.221086787579271 0 -27.870845195179363 -0.5737357863269259 27.022279980474448 2.308891823505762 0.07083333333333333 0.5 0.7280891517516188 1.0 -28.576168692386503 -0.5737357863269259 38.245956935564664 2.149722785033046 0.12552083333333333 0.5018518518518519 0.0 1.0 -26.761751693277454 -0.5737357863269259 32.725978757409216 2.1935086988816606 0.10885416666666667 0.5009259259259259 0.6296276745572366 1.0 -29.685262194288413 -0.5737357863269259 32.54225815862989 2.2475546793311034 0.09010416666666667 0.5018518518518519 0.0 1.0 0.92
|
||||
0 0.47313958333333334 0.5319606481481481 0.00781874999999997 0.026975000000000034 -3.346141769945419 1.1405364422438105 73.97580981833369 4.276902474722242 1.6040241866108331 1.8584808008962146 -1.668303332348193 0.4786458333333333 0.5370370370370371 0.4791666666666667 0.5518518518518518 -1.6231012252264567 2 -3.5543254918500575 1.1405364422438105 76.10410332076826 -1.6216337775450165 0.47760416666666666 0.5361111111111111 0.0 1.0 -3.1379580480407796 1.1405364422438105 71.84751631589911 -1.6246558238118147 0.48020833333333335 0.537962962962963 0.982706456307913 1.0 -4.270968238709756 1.1405364422438105 73.88534587801576 -1.610562238832687 0.46927083333333336 0.5370370370370371 0.15910928907354044 -1.0 -2.421315301181082 1.1405364422438105 74.06627375865162 -1.6356237716506252 0.48854166666666665 0.5370370370370371 0.0 -1.0 0.91
|
||||
0 0.2927854166666667 0.5616333333333334 0.09309479166666668 0.11907222222222229 -5.688442457002822 0.6515146009446231 18.847581783882784 4.572232225876627 1.6314661906678796 2.032986999088073 -1.6067899425915653 0.30364583333333334 0.5592592592592592 0.3046875 0.612037037037037 -1.3136708098830607 2 -5.770710275806388 0.6515146009446231 21.132217178825595 -1.3402129439854304 0.32083333333333336 0.5546296296296296 0.0 1.0 -5.6061746381992545 0.6515146009446231 16.562946388939974 -1.2804173492789355 0.2833333333333333 0.5638888888888889 0.9028504867548437 1.0 -6.704277573445244 0.6515146009446231 18.81100240693766 -1.2644232676909533 0.2734375 0.5574074074074075 0.0 -1.0 -4.6726073405604 0.6515146009446231 18.884161160827908 -1.3642266045932105 0.33645833333333336 0.5601851851851852 0.3626380696039398 -1.0 0.91
|
||||
7 0.5116114583333333 0.46365925925925927 0.025406249999999984 0.028024074074074038 -1 0.91
|
||||
0 0.46437369791666666 0.5231402777777777 0.008083854166666645 0.018084259259259213 -1 0.91
|
||||
0 0.3746828125 0.5191787037037038 0.011252083333333355 0.010085185185185233 -1 0.89
|
||||
0 0.42897421874999997 0.5274851851851852 0.00588385416666668 0.017529629629629645 -1 0.89
|
||||
9 0.6749369791666666 0.717326388888889 0.01799479166666664 0.11860462962962959 -1 0.88
|
||||
8 0.7758239583333334 0.6332930555555556 0.03819895833333339 0.030591666666666656 -1 0.88
|
||||
9 0.6255473958333333 0.6657171296296297 0.009577083333333292 0.08832870370370369 -1 0.87
|
||||
0 0.7247682291666667 0.6297800925925925 0.20937604166666665 0.294475 2.434552068275443 0.5967205147944805 8.058303786791164 4.350524874593896 1.6536647566476377 2.0479167613181763 -1.6151998089257185 0.7223958333333333 0.6074074074074074 0.7182291666666667 0.7287037037037037 -1.908597842744447 2 2.3379945787164735 0.5967205147944805 10.231422127407967 -1.8398537412718614 0.6776041666666667 0.5907407407407408 0.0 1.0 2.5311095578344127 0.5967205147944805 5.885185446174361 -2.0213666722443397 0.7880208333333333 0.6314814814814815 0.844486831895201 1.0 1.4116029753817916 0.5967205147944805 8.012851408745153 -1.78957793404185 0.6427083333333333 0.6138888888888889 0.3732032917911752 -1.0 3.4575011611690947 0.5967205147944805 8.103756164837176 -2.018470697720395 0.7875 0.6 0.0 -1.0 0.87
|
||||
8 0.2723901041666667 0.5561902777777779 0.016830208333333315 0.012158333333333307 -1 0.86
|
||||
8 0.49003854166666666 0.5378004629629629 0.008782291666666648 0.005245370370370337 -1 0.86
|
||||
8 0.557528125 0.5356800925925925 0.010732291666666664 0.006441666666666661 -1 0.85
|
||||
0 0.39026536458333333 0.5196268518518518 0.010867187500000005 0.008009259259259239 -1 0.85
|
||||
9 0.3358854166666667 0.5885976851851852 0.005460416666666686 0.04338240740740736 -1 0.85
|
||||
9 0.3146812499999999 0.5978162037037037 0.007009374999999984 0.04718796296296293 -1 0.84
|
||||
0 0.31288854166666674 0.5064208333333334 0.029720833333333314 0.0355490740740741 -1 0.84
|
||||
2 0.8897486979166668 0.571062962962963 0.03627968749999996 0.09224814814814819 8.433443163792141 0.5561401950767102 10.904898338138215 1.5817150292535551 1.1590158531086248 0.6559058475241687 2.2699693057766948 0.9072916666666667 0.562037037037037 0.9057291666666667 0.6101851851851852 1.6116828731051827 -1 0.84
|
||||
0 0.15874635416666666 0.5144449074074074 0.05282708333333333 0.07640092592592596 -21.61443310326651 0.6272993643326822 33.165049334711064 4.393192427520202 1.6734817142825604 1.9250059218333837 1.5111592429980747 0.15208333333333332 0.5351851851851852 0.15260416666666668 0.5592592592592592 2.0887449199425503 0 -21.483512148192084 0.6272993643326823 30.972358150383545 2.117600620303752 0.13958333333333334 0.5351851851851852 0.8073992662473614 1.0 -21.745354058340936 0.6272993643326823 35.35774051903858 2.062542259413652 0.16354166666666667 0.5342592592592592 0.0 1.0 -20.653641245359005 0.6272993643326823 33.22241618534834 2.067366050077159 0.16145833333333334 0.5351851851851852 0.562250926367033 1.0 -22.575224961174015 0.6272993643326823 33.10768248407379 2.109615354828887 0.14322916666666666 0.5342592592592592 0.0 1.0 0.83
|
||||
2 0.8415385416666666 0.5651731481481481 0.02687291666666667 0.07048703703703701 8.274189751848002 0.6023878510463183 15.041911630616367 1.5969253707069795 1.1521873497832582 0.6506352301708902 -2.3274015159349246 0.8395833333333333 0.5583333333333333 0.8390625 0.5990740740740741 -2.830302827792134 -1 0.79
|
||||
2 0.9305695312500001 0.5717587962962962 0.04436510416666669 0.08631388888888883 8.57433515265391 0.499378573554066 9.216428684195302 1.613578829060235 1.1916644060725476 0.666386172630037 -2.173991236345734 0.9390625 0.5601851851851852 0.9375 0.6129629629629629 -2.923313660642581 -1 0.79
|
||||
5 0.6147372395833334 0.5129231481481481 0.00301510416666666 0.0058499999999999846 -1 0.74
|
||||
5 0.6297578125 0.5220236111111111 0.002812500000000047 0.005423148148148121 -1 0.71
|
||||
5 0.6562223958333333 0.5084175925925926 0.0033177083333332763 0.007281481481481512 -1 0.69
|
||||
5 0.6463380208333334 0.5099624999999999 0.004018749999999945 0.00903425925925921 -1 0.69
|
||||
1 0.6151989583333334 0.5296060185185185 0.007279166666666725 0.039167592592592636 7.347362924447552 0.5188912729640653 53.535415266887306 0.621501340976369 1.6793547489833478 0.6376270827755769 1.392885077955592 0.615625 0.5287037037037037 0.6161458333333333 0.55 1.2564941201312294 -1 0.67
|
||||
0 0.34099843750000003 0.510698611111111 0.021696875000000008 0.02038796296296297 -1 0.66
|
||||
1 0.6062763020833334 0.5306763888888889 0.006914062500000048 0.04047314814814816 6.684211922727918 0.50565346419902 52.309795522222025 0.6372101484471651 1.7148569604954305 0.661297859795197 1.4078590097619292 0.6088541666666667 0.5287037037037037 0.6088541666666667 0.5509259259259259 1.2807664813456126 -1 0.66
|
||||
5 0.6063796875 0.5144546296296296 0.0034864583333333123 0.0063851851851851466 -1 0.66
|
||||
5 0.6237174479166666 0.5085476851851851 0.0036546875000000273 0.006847222222222205 -1 0.63
|
||||
0 0.3362307291666667 0.522824074074074 0.016811458333333345 0.022809259259259272 -1 0.58
|
||||
0 0.21077135416666667 0.5157 0.01811562499999999 0.0124092592592593 -1 0.57
|
||||
0 0.010891927083333334 0.5274768518518518 0.015023437499999999 0.07451851851851854 -28.552226792399612 0.49877847130119835 20.052826453506107 4.421758635933981 1.635324433067478 1.9300756313973146 1.5296674075878407 0.013541666666666667 0.5314814814814814 0.0140625 0.5574074074074075 2.4881817971862157 0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.0 -1.0 -28.643132235093383 0.4987784713011984 22.261836086600276 2.4397720971159007 0.0359375 0.5305555555555556 1.0 1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 0.0 -1.0 0.54
|
||||
0 0.19245078125 0.5186064814814815 0.015506770833333346 0.008390740740740752 -1 0.53
|
||||
0 0.36203151041666665 0.5207194444444444 0.013595312499999975 0.024218518518518473 -1 0.52
|
||||
1 0.6457776041666666 0.5293657407407407 0.008578125000000015 0.04807222222222223 7.264055455404972 0.4250015235932112 38.97695214074227 0.6340940521295699 1.6317781099034117 0.6495978470487854 1.520944803155322 0.65 0.5305555555555556 0.65 0.5574074074074075 1.3366906605014859 -1 0.5
|
||||
8 0.4186299479166667 0.5273449074074075 0.004230729166666692 0.002800925925925905 -1 0.49
|
||||
1 0.6234333333333334 0.5265527777777778 0.006395833333333319 0.04297407407407411 7.435751816404869 0.5914538128258182 51.34862219983651 0.6215427178226364 1.6906295073461521 0.6293591509621058 -1.7423619244841528 0.6213541666666667 0.5314814814814814 0.6213541666666667 0.5537037037037037 -1.8861714493903388 -1 0.48
|
||||
0 0.23057760416666667 0.5147611111111111 0.030210416666666677 0.014929629629629652 -1 0.47
|
||||
5 0.5779479166666667 0.5136111111111111 0.0028249999999999885 0.005500000000000051 -1 0.46
|
||||
0 0.35159765625000006 0.5188657407407408 0.008054687500000017 0.00868148148148146 -1 0.45
|
||||
8 0.4477528645833333 0.5327592592592592 0.005349479166666645 0.0034777777777777515 -1 0.43
|
||||
1 0.6304580729166667 0.5335208333333332 0.009071354166666623 0.02864722222222219 7.435045285601043 0.6536059457746322 47.49907910714277 0.6325653300177067 1.3389352886791703 0.6131565073809939 2.999690514771298 0.6296875 0.5342592592592592 0.6296875 0.5527777777777778 2.8444201609579514 -1 0.4
|
||||
5 0.8994692708333333 0.48519537037037036 0.0030354166666666533 0.007194444444444428 -1 0.4
|
||||
0 0.45893307291666663 0.5263041666666667 0.003116145833333306 0.008612037037037078 -1 0.38
|
||||
0 0.23074557291666664 0.5255541666666668 0.032185937499999984 0.03686203703703707 -1 0.37
|
||||
0 0.03305703125 0.5166648148148149 0.027794270833333336 0.014185185185185231 -1 0.36
|
||||
1 0.8995971354166666 0.4979037037037037 0.005455729166666619 0.03278888888888892 -1 0.34
|
||||
0 0.4071604166666667 0.5223296296296296 0.008616666666666658 0.009635185185185137 -1 0.34
|
||||
0 0.011420052083333333 0.5057291666666667 0.012668229166666666 0.02998240740740738 -1 0.32
|
||||
5 0.6049046874999999 0.5127273148148148 0.0031270833333332843 0.006182407407407426 -1 0.32
|
||||
0 0.35157239583333333 0.5235708333333333 0.008809374999999993 0.018156481481481516 -1 0.31
|
||||
6 0.5291265625 0.5049412037037037 0.0037979166666666825 0.006150925925925953 -1 0.3
|
||||
1053
tools/convert_gt_to_label/package_visualization_batches.py
Executable file
1053
tools/convert_gt_to_label/package_visualization_batches.py
Executable file
File diff suppressed because it is too large
Load Diff
53
tools/convert_gt_to_label/package_visualization_batches.sh
Executable file
53
tools/convert_gt_to_label/package_visualization_batches.sh
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
|
||||
PYTHON_BIN="${PYTHON_BIN:-/deeplearning_team/ydong/dongying/miniconda/envs/dev/bin/python}"
|
||||
|
||||
detect_cpu_count() {
|
||||
local cpu_count
|
||||
cpu_count="$(getconf _NPROCESSORS_ONLN 2>/dev/null || nproc 2>/dev/null || echo 1)"
|
||||
if [[ ! "${cpu_count}" =~ ^[0-9]+$ ]] || (( cpu_count <= 0 )); then
|
||||
cpu_count=1
|
||||
fi
|
||||
echo "${cpu_count}"
|
||||
}
|
||||
|
||||
calc_scan_workers() {
|
||||
local cpu_count="$1"
|
||||
if (( cpu_count >= 8 )); then
|
||||
echo 8
|
||||
else
|
||||
echo "${cpu_count}"
|
||||
fi
|
||||
}
|
||||
|
||||
calc_part_workers() {
|
||||
local cpu_count="$1"
|
||||
if (( cpu_count >= 32 )); then
|
||||
echo 4
|
||||
elif (( cpu_count >= 16 )); then
|
||||
echo 3
|
||||
elif (( cpu_count >= 8 )); then
|
||||
echo 2
|
||||
else
|
||||
echo 1
|
||||
fi
|
||||
}
|
||||
|
||||
CPU_COUNT="$(detect_cpu_count)"
|
||||
|
||||
# 打包是 I/O + gzip 压缩混合负载,这里默认给一套偏稳妥的并发配置;
|
||||
# 如需手动调节,可在命令前覆盖这些环境变量。
|
||||
VIS_PACK_SCAN_WORKERS="${VIS_PACK_SCAN_WORKERS:-$(calc_scan_workers "${CPU_COUNT}")}"
|
||||
VIS_PACK_ARCHIVE_WORKERS="${VIS_PACK_ARCHIVE_WORKERS:-2}"
|
||||
VIS_PACK_PART_WORKERS="${VIS_PACK_PART_WORKERS:-$(calc_part_workers "${CPU_COUNT}")}"
|
||||
|
||||
exec "${PYTHON_BIN}" \
|
||||
"${PROJECT_ROOT}/tools/convert_gt_to_label/package_visualization_batches.py" \
|
||||
--scan-workers "${VIS_PACK_SCAN_WORKERS}" \
|
||||
--archive-workers "${VIS_PACK_ARCHIVE_WORKERS}" \
|
||||
--part-workers "${VIS_PACK_PART_WORKERS}" \
|
||||
"$@"
|
||||
498
tools/convert_gt_to_label/rewrite_visualization_archives.py
Executable file
498
tools/convert_gt_to_label/rewrite_visualization_archives.py
Executable file
@@ -0,0 +1,498 @@
|
||||
#!/usr/bin/env python3
|
||||
# coding: utf-8
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import tarfile
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from datetime import datetime
|
||||
from pathlib import Path, PurePosixPath
|
||||
|
||||
|
||||
MODALITIES = ("2D", "3D")
|
||||
DEFAULT_ARCHIVE_GLOB = "part_*.tar.gz"
|
||||
DEFAULT_REPORT_NAME = "rewrite_archives_report.json"
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"将已有可视化归档中的成员路径改写为 part_xxxx/images/<filename>,"
|
||||
"并输出到新的归档目录。"
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
"source_path",
|
||||
help="输入路径,可为 archive root、单个 modality 目录,或单个 tar/tar.gz 文件。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"output_root",
|
||||
nargs="?",
|
||||
default=None,
|
||||
help="输出目录。默认在 source_path 同级生成 <name>_images_prefixed。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--modalities",
|
||||
default="2D,3D",
|
||||
help="当 source_path 为 archive root 时要处理的模态,逗号分隔,默认 2D,3D。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--archive-glob",
|
||||
default=DEFAULT_ARCHIVE_GLOB,
|
||||
help="归档匹配模式,默认 part_*.tar.gz。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--parts",
|
||||
default=None,
|
||||
help=(
|
||||
"仅处理指定 part,逗号分隔;既支持 part_0001,也支持 part_0001.tar.gz。"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--limit",
|
||||
type=int,
|
||||
default=None,
|
||||
help="最多处理多少个归档,便于抽样验证。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--workers",
|
||||
type=int,
|
||||
default=1,
|
||||
help="并发处理归档数,默认 1。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--gzip-compresslevel",
|
||||
type=int,
|
||||
default=1,
|
||||
help="输出 tar.gz 的 gzip 压缩级别,范围 0-9,默认 1。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--checksum",
|
||||
action="store_true",
|
||||
help="输出归档后计算 sha256。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--verify",
|
||||
action="store_true",
|
||||
default=True,
|
||||
help="输出归档后校验成员数及路径前缀,默认开启。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-verify",
|
||||
dest="verify",
|
||||
action="store_false",
|
||||
help="关闭输出归档校验。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--overwrite",
|
||||
action="store_true",
|
||||
help="允许覆盖已有输出归档。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--report-name",
|
||||
default=DEFAULT_REPORT_NAME,
|
||||
help=f"输出报告文件名,默认 {DEFAULT_REPORT_NAME}。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="仅输出转换计划,不实际生成归档。",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def now_str():
|
||||
return datetime.now().isoformat(timespec="seconds")
|
||||
|
||||
|
||||
def normalize_modalities(raw_modalities):
|
||||
modalities = []
|
||||
for item in raw_modalities.split(","):
|
||||
modality = item.strip()
|
||||
if not modality:
|
||||
continue
|
||||
if modality not in MODALITIES:
|
||||
raise ValueError(f"unsupported modality: {modality}")
|
||||
modalities.append(modality)
|
||||
if not modalities:
|
||||
raise ValueError("modalities 不能为空。")
|
||||
return tuple(dict.fromkeys(modalities))
|
||||
|
||||
|
||||
def archive_suffix(path):
|
||||
name = path.name
|
||||
if name.endswith(".tar.gz"):
|
||||
return ".tar.gz"
|
||||
if name.endswith(".tar"):
|
||||
return ".tar"
|
||||
raise ValueError(f"unsupported archive suffix: {path}")
|
||||
|
||||
|
||||
def strip_archive_suffix(name):
|
||||
if name.endswith(".tar.gz"):
|
||||
return name[: -len(".tar.gz")]
|
||||
if name.endswith(".tar"):
|
||||
return name[: -len(".tar")]
|
||||
return name
|
||||
|
||||
|
||||
def parse_parts_filter(raw_parts):
|
||||
if not raw_parts:
|
||||
return None
|
||||
normalized = []
|
||||
for item in raw_parts.split(","):
|
||||
value = item.strip()
|
||||
if not value:
|
||||
continue
|
||||
normalized.append(strip_archive_suffix(value))
|
||||
if not normalized:
|
||||
return None
|
||||
return set(normalized)
|
||||
|
||||
|
||||
def is_archive_file(path):
|
||||
return path.is_file() and path.name.endswith((".tar", ".tar.gz"))
|
||||
|
||||
|
||||
def default_output_root(source_path, resolve_mode):
|
||||
source_path = source_path.resolve()
|
||||
if resolve_mode == "single_archive":
|
||||
base_name = strip_archive_suffix(source_path.name)
|
||||
else:
|
||||
base_name = source_path.name
|
||||
return source_path.parent / f"{base_name}_images_prefixed"
|
||||
|
||||
|
||||
def resolve_archives(source_path, archive_glob, modalities):
|
||||
source_path = source_path.resolve()
|
||||
|
||||
if source_path.is_file():
|
||||
if not is_archive_file(source_path):
|
||||
raise ValueError(f"source_path is not a supported archive file: {source_path}")
|
||||
return "single_archive", [source_path], source_path.parent
|
||||
|
||||
if not source_path.is_dir():
|
||||
raise FileNotFoundError(f"source_path does not exist or is not a directory: {source_path}")
|
||||
|
||||
direct_archives = sorted(path for path in source_path.glob(archive_glob) if is_archive_file(path))
|
||||
if source_path.name in MODALITIES and direct_archives:
|
||||
return "modality_dir", direct_archives, source_path.parent
|
||||
|
||||
archive_paths = []
|
||||
for modality in modalities:
|
||||
modality_dir = source_path / modality
|
||||
if not modality_dir.is_dir():
|
||||
continue
|
||||
archive_paths.extend(
|
||||
sorted(path for path in modality_dir.glob(archive_glob) if is_archive_file(path))
|
||||
)
|
||||
if archive_paths:
|
||||
return "archive_root", archive_paths, source_path
|
||||
|
||||
raise FileNotFoundError(
|
||||
f"no archives found under {source_path} with archive_glob={archive_glob}"
|
||||
)
|
||||
|
||||
|
||||
def filter_archives(archive_paths, parts_filter, limit):
|
||||
selected = archive_paths
|
||||
if parts_filter:
|
||||
selected = [
|
||||
path for path in selected if strip_archive_suffix(path.name) in parts_filter
|
||||
]
|
||||
if limit is not None:
|
||||
selected = selected[:limit]
|
||||
if not selected:
|
||||
raise FileNotFoundError("没有匹配到待处理归档,请检查 parts 或 archive_glob。")
|
||||
return selected
|
||||
|
||||
|
||||
def build_output_archive_path(archive_path, source_base_dir, output_root):
|
||||
return output_root / archive_path.relative_to(source_base_dir)
|
||||
|
||||
|
||||
def build_archive_member_dir(archive_path):
|
||||
return f"{strip_archive_suffix(archive_path.name)}/images"
|
||||
|
||||
|
||||
def build_output_member_name(member_name, output_archive):
|
||||
return f"{build_archive_member_dir(output_archive)}/{PurePosixPath(member_name).name}"
|
||||
|
||||
|
||||
def open_output_tarfile(output_archive, archive_format, gzip_compresslevel):
|
||||
suffix = archive_format or archive_suffix(output_archive)
|
||||
if suffix == ".tar.gz":
|
||||
return tarfile.open(
|
||||
output_archive,
|
||||
mode="w:gz",
|
||||
compresslevel=gzip_compresslevel,
|
||||
)
|
||||
return tarfile.open(output_archive, mode="w")
|
||||
|
||||
|
||||
def compute_sha256(file_path):
|
||||
digest = hashlib.sha256()
|
||||
with open(file_path, "rb") as f:
|
||||
while True:
|
||||
chunk = f.read(1024 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def verify_rewritten_archive(archive_path, expected_member_count):
|
||||
member_count = 0
|
||||
samples = []
|
||||
expected_prefix = f"{build_archive_member_dir(archive_path)}/"
|
||||
with tarfile.open(archive_path, mode="r:*") as tar_obj:
|
||||
for member in tar_obj:
|
||||
if not member.isfile():
|
||||
raise ValueError(
|
||||
f"rewritten archive contains non-regular member: {archive_path} -> {member.name}"
|
||||
)
|
||||
if not member.name.startswith(expected_prefix):
|
||||
raise ValueError(
|
||||
f"rewritten archive member missing expected prefix {expected_prefix}: "
|
||||
f"{archive_path} -> {member.name}"
|
||||
)
|
||||
member_count += 1
|
||||
if len(samples) < 5:
|
||||
samples.append(member.name)
|
||||
|
||||
if member_count != expected_member_count:
|
||||
raise ValueError(
|
||||
f"rewritten archive member count mismatch: {archive_path}, "
|
||||
f"expected={expected_member_count}, actual={member_count}"
|
||||
)
|
||||
return {
|
||||
"member_count": member_count,
|
||||
"sample_members": samples,
|
||||
}
|
||||
|
||||
|
||||
def rewrite_archive(
|
||||
source_archive,
|
||||
output_archive,
|
||||
gzip_compresslevel,
|
||||
verify,
|
||||
checksum,
|
||||
overwrite,
|
||||
dry_run,
|
||||
):
|
||||
source_archive = source_archive.resolve()
|
||||
output_archive = output_archive.resolve()
|
||||
if output_archive.exists() and not overwrite:
|
||||
raise FileExistsError(f"output archive already exists: {output_archive}")
|
||||
|
||||
samples_before = []
|
||||
samples_after = []
|
||||
|
||||
if dry_run:
|
||||
return {
|
||||
"status": "dry_run",
|
||||
"source_archive": str(source_archive),
|
||||
"output_archive": str(output_archive),
|
||||
"member_count": None,
|
||||
"sample_members_before": samples_before,
|
||||
"sample_members_after": samples_after,
|
||||
"output_sha256": None,
|
||||
"verification": None,
|
||||
}
|
||||
|
||||
output_archive.parent.mkdir(parents=True, exist_ok=True)
|
||||
partial_archive = output_archive.with_name(f"{output_archive.name}.partial")
|
||||
if partial_archive.exists():
|
||||
partial_archive.unlink()
|
||||
|
||||
seen_output_names = {}
|
||||
member_count = 0
|
||||
output_archive_format = archive_suffix(output_archive)
|
||||
try:
|
||||
with tarfile.open(source_archive, mode="r:*") as input_tar:
|
||||
with open_output_tarfile(
|
||||
partial_archive,
|
||||
output_archive_format,
|
||||
gzip_compresslevel,
|
||||
) as output_tar:
|
||||
for member in input_tar:
|
||||
if not member.isfile():
|
||||
raise ValueError(
|
||||
f"only regular file members are supported: "
|
||||
f"{source_archive} -> {member.name}"
|
||||
)
|
||||
|
||||
output_name = build_output_member_name(member.name, output_archive)
|
||||
if output_name in seen_output_names:
|
||||
raise ValueError(
|
||||
"duplicated output member name after rewrite: "
|
||||
f"{output_name}, first={seen_output_names[output_name]}, "
|
||||
f"duplicate={member.name}"
|
||||
)
|
||||
seen_output_names[output_name] = member.name
|
||||
|
||||
if len(samples_before) < 5:
|
||||
samples_before.append(member.name)
|
||||
if len(samples_after) < 5:
|
||||
samples_after.append(output_name)
|
||||
|
||||
input_file = input_tar.extractfile(member)
|
||||
if input_file is None:
|
||||
raise ValueError(
|
||||
f"failed to extract file object from member: {source_archive} -> {member.name}"
|
||||
)
|
||||
|
||||
output_member = copy.copy(member)
|
||||
output_member.name = output_name
|
||||
if output_member.pax_headers:
|
||||
output_member.pax_headers = dict(output_member.pax_headers)
|
||||
|
||||
try:
|
||||
output_tar.addfile(output_member, input_file)
|
||||
finally:
|
||||
input_file.close()
|
||||
|
||||
member_count += 1
|
||||
|
||||
partial_archive.replace(output_archive)
|
||||
finally:
|
||||
if partial_archive.exists():
|
||||
partial_archive.unlink()
|
||||
|
||||
verification = verify_rewritten_archive(output_archive, member_count) if verify else None
|
||||
output_sha256 = compute_sha256(output_archive) if checksum else None
|
||||
return {
|
||||
"status": "rewritten",
|
||||
"source_archive": str(source_archive),
|
||||
"output_archive": str(output_archive),
|
||||
"member_count": member_count,
|
||||
"sample_members_before": samples_before,
|
||||
"sample_members_after": samples_after,
|
||||
"output_sha256": output_sha256,
|
||||
"verification": verification,
|
||||
}
|
||||
|
||||
|
||||
def rewrite_archives(archive_paths, source_base_dir, output_root, args):
|
||||
tasks = [
|
||||
(archive_path, build_output_archive_path(archive_path, source_base_dir, output_root))
|
||||
for archive_path in archive_paths
|
||||
]
|
||||
if args.workers <= 1 or len(tasks) <= 1:
|
||||
return [
|
||||
rewrite_archive(
|
||||
source_archive,
|
||||
output_archive,
|
||||
args.gzip_compresslevel,
|
||||
args.verify,
|
||||
args.checksum,
|
||||
args.overwrite,
|
||||
args.dry_run,
|
||||
)
|
||||
for source_archive, output_archive in tasks
|
||||
]
|
||||
|
||||
results = [None] * len(tasks)
|
||||
with ThreadPoolExecutor(max_workers=min(args.workers, len(tasks))) as executor:
|
||||
future_map = {
|
||||
executor.submit(
|
||||
rewrite_archive,
|
||||
source_archive,
|
||||
output_archive,
|
||||
args.gzip_compresslevel,
|
||||
args.verify,
|
||||
args.checksum,
|
||||
args.overwrite,
|
||||
args.dry_run,
|
||||
): index
|
||||
for index, (source_archive, output_archive) in enumerate(tasks)
|
||||
}
|
||||
for future in as_completed(future_map):
|
||||
index = future_map[future]
|
||||
results[index] = future.result()
|
||||
return results
|
||||
|
||||
|
||||
def build_report(args, resolve_mode, source_path, output_root, archive_paths, results):
|
||||
return {
|
||||
"generated_at": now_str(),
|
||||
"source_path": str(Path(source_path).resolve()),
|
||||
"output_root": str(Path(output_root).resolve()),
|
||||
"resolve_mode": resolve_mode,
|
||||
"modalities": list(normalize_modalities(args.modalities)),
|
||||
"archive_glob": args.archive_glob,
|
||||
"parts": sorted(parse_parts_filter(args.parts) or []),
|
||||
"limit": args.limit,
|
||||
"workers": args.workers,
|
||||
"gzip_compresslevel": args.gzip_compresslevel,
|
||||
"checksum": args.checksum,
|
||||
"verify": args.verify,
|
||||
"overwrite": args.overwrite,
|
||||
"dry_run": args.dry_run,
|
||||
"selected_archive_count": len(archive_paths),
|
||||
"results": results,
|
||||
}
|
||||
|
||||
|
||||
def write_json(path, data):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
if args.workers <= 0:
|
||||
raise ValueError("workers 必须大于 0")
|
||||
if not 0 <= args.gzip_compresslevel <= 9:
|
||||
raise ValueError("gzip-compresslevel 必须在 0 到 9 之间")
|
||||
|
||||
source_path = Path(args.source_path).resolve()
|
||||
modalities = normalize_modalities(args.modalities)
|
||||
parts_filter = parse_parts_filter(args.parts)
|
||||
|
||||
resolve_mode, archive_paths, source_base_dir = resolve_archives(
|
||||
source_path,
|
||||
args.archive_glob,
|
||||
modalities,
|
||||
)
|
||||
archive_paths = filter_archives(archive_paths, parts_filter, args.limit)
|
||||
|
||||
output_root = (
|
||||
Path(args.output_root).resolve()
|
||||
if args.output_root
|
||||
else default_output_root(source_path, resolve_mode)
|
||||
)
|
||||
|
||||
print("")
|
||||
print("######################################################################")
|
||||
print("# Rewrite visualization archives")
|
||||
print("######################################################################")
|
||||
print(f"Source path : {source_path}")
|
||||
print(f"Output root : {output_root}")
|
||||
print(f"Resolve mode : {resolve_mode}")
|
||||
print(f"Selected count : {len(archive_paths)}")
|
||||
print(f"Workers : {args.workers}")
|
||||
print(f"Dry run : {args.dry_run}")
|
||||
|
||||
results = rewrite_archives(archive_paths, source_base_dir, output_root, args)
|
||||
report = build_report(args, resolve_mode, source_path, output_root, archive_paths, results)
|
||||
report_path = output_root / args.report_name
|
||||
write_json(report_path, report)
|
||||
|
||||
print(f"Report : {report_path}")
|
||||
for item in results:
|
||||
print(
|
||||
f"[{item['status']}] {item['source_archive']} -> {item['output_archive']}"
|
||||
)
|
||||
if item["sample_members_before"]:
|
||||
print(f" before: {item['sample_members_before'][:3]}")
|
||||
print(f" after : {item['sample_members_after'][:3]}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
40
tools/convert_gt_to_label/rewrite_visualization_archives.sh
Executable file
40
tools/convert_gt_to_label/rewrite_visualization_archives.sh
Executable file
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
|
||||
PYTHON_BIN="${PYTHON_BIN:-/deeplearning_team/ydong/dongying/miniconda/envs/dev/bin/python}"
|
||||
|
||||
detect_cpu_count() {
|
||||
local cpu_count
|
||||
cpu_count="$(getconf _NPROCESSORS_ONLN 2>/dev/null || nproc 2>/dev/null || echo 1)"
|
||||
if [[ ! "${cpu_count}" =~ ^[0-9]+$ ]] || (( cpu_count <= 0 )); then
|
||||
cpu_count=1
|
||||
fi
|
||||
echo "${cpu_count}"
|
||||
}
|
||||
|
||||
calc_workers() {
|
||||
local cpu_count="$1"
|
||||
if (( cpu_count >= 16 )); then
|
||||
echo 4
|
||||
elif (( cpu_count >= 8 )); then
|
||||
echo 2
|
||||
else
|
||||
echo 1
|
||||
fi
|
||||
}
|
||||
|
||||
CPU_COUNT="$(detect_cpu_count)"
|
||||
|
||||
# 归档重写主要是串行读取 + gzip 写出,默认给一套偏稳妥的并发配置;
|
||||
# 如需手动调节,可在命令前覆盖这些环境变量。
|
||||
VIS_REWRITE_WORKERS="${VIS_REWRITE_WORKERS:-$(calc_workers "${CPU_COUNT}")}"
|
||||
VIS_REWRITE_GZIP_LEVEL="${VIS_REWRITE_GZIP_LEVEL:-1}"
|
||||
|
||||
exec "${PYTHON_BIN}" \
|
||||
"${PROJECT_ROOT}/tools/convert_gt_to_label/rewrite_visualization_archives.py" \
|
||||
--workers "${VIS_REWRITE_WORKERS}" \
|
||||
--gzip-compresslevel "${VIS_REWRITE_GZIP_LEVEL}" \
|
||||
"$@"
|
||||
27
tools/convert_gt_to_label/run_convert_json_to_json.sh
Executable file
27
tools/convert_gt_to_label/run_convert_json_to_json.sh
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PYTHON_SCRIPT="${SCRIPT_DIR}/convert_json_to_json.py"
|
||||
DEFAULT_DEV_PYTHON="/deeplearning_team/ydong/dongying/miniconda/envs/dev/bin/python"
|
||||
PYTHON_BIN="${PYTHON_BIN:-}"
|
||||
|
||||
if [[ -z "${PYTHON_BIN}" ]]; then
|
||||
if [[ -x "${DEFAULT_DEV_PYTHON}" ]]; then
|
||||
PYTHON_BIN="${DEFAULT_DEV_PYTHON}"
|
||||
else
|
||||
PYTHON_BIN="python3"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "Usage: $0 <json_root> [extra args...]"
|
||||
echo "Example: $0 /data1/dongying/Mono3d/G1M3/Testdata_0129/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
JSON_ROOT="$1"
|
||||
shift
|
||||
|
||||
"${PYTHON_BIN}" "${PYTHON_SCRIPT}" --json-root "${JSON_ROOT}" "$@"
|
||||
23
tools/convert_gt_to_label/run_convert_json_to_json_test.sh
Executable file
23
tools/convert_gt_to_label/run_convert_json_to_json_test.sh
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
DEFAULT_DEV_PYTHON="/deeplearning_team/ydong/dongying/miniconda/envs/dev/bin/python"
|
||||
PYTHON_BIN="${PYTHON_BIN:-}"
|
||||
if [[ -z "${PYTHON_BIN}" ]]; then
|
||||
if [[ -x "${DEFAULT_DEV_PYTHON}" ]]; then
|
||||
PYTHON_BIN="${DEFAULT_DEV_PYTHON}"
|
||||
else
|
||||
PYTHON_BIN="python3"
|
||||
fi
|
||||
fi
|
||||
|
||||
CASE_DIR="/data1/dongying/Mono3d/G1Q3/dataset_for_evaluation/OP_KPI_SCENE/019b1a9c-6a6a-7523-b69c-748bdbd74d33"
|
||||
INPUT_JSON="${CASE_DIR}/annotations/G1M3_2232_20251212195850_019b1a9c-6a6a-7523-b69c-748bdbd74d33_000001_49704_1765542681704437000.json"
|
||||
OUTPUT_JSON="${CASE_DIR}/annotations_converted/G1M3_2232_20251212195850_019b1a9c-6a6a-7523-b69c-748bdbd74d33_000001_49704_1765542681704437000_converted.json"
|
||||
CALIB_DIR="${CASE_DIR}/calib"
|
||||
|
||||
"${PYTHON_BIN}" tools/convert_gt_to_label/convert_json_to_json.py \
|
||||
"${INPUT_JSON}" \
|
||||
"${OUTPUT_JSON}" \
|
||||
--calib-path "${CALIB_DIR}"
|
||||
172
tools/convert_gt_to_label/stat_gt_categories.py
Executable file
172
tools/convert_gt_to_label/stat_gt_categories.py
Executable file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env python3
|
||||
# coding: utf-8
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
from collections import Counter
|
||||
|
||||
|
||||
DEFAULT_JSON_ROOT = "/data1/dongying/Mono3d/G1M3/Testdata_0129"
|
||||
DEFAULT_GLOB_PATTERN = os.path.join(DEFAULT_JSON_ROOT, "*", "annotations_20260320", "*.json")
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="统计原始真值中的 camera_mea.cls 类别,并生成映射模板。"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--json-root",
|
||||
default=DEFAULT_JSON_ROOT,
|
||||
help="原始标注根目录。若未显式传 glob-pattern,则会自动拼接默认 pattern。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--glob-pattern",
|
||||
default=None,
|
||||
help="JSON 搜索 pattern,优先级高于 json-root。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
default=os.path.join(os.path.dirname(__file__), "category_stats_output"),
|
||||
help="统计结果输出目录。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--limit",
|
||||
type=int,
|
||||
default=0,
|
||||
help="仅统计前 N 个 JSON,0 表示统计全部。",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sample-per-category",
|
||||
type=int,
|
||||
default=5,
|
||||
help="每个类别保留的样例数量。",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def canonicalize_cls(raw_cls):
|
||||
if isinstance(raw_cls, str):
|
||||
return raw_cls
|
||||
if raw_cls is None:
|
||||
return "<missing>"
|
||||
try:
|
||||
return json.dumps(raw_cls, ensure_ascii=False, sort_keys=True)
|
||||
except TypeError:
|
||||
return str(raw_cls)
|
||||
|
||||
|
||||
def build_mapping_template(categories):
|
||||
return {category: None for category in categories}
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
glob_pattern = args.glob_pattern or os.path.join(
|
||||
args.json_root, "*", "annotations_20260320", "*.json"
|
||||
)
|
||||
|
||||
json_files = sorted(glob.glob(glob_pattern))
|
||||
if args.limit > 0:
|
||||
json_files = json_files[: args.limit]
|
||||
|
||||
if not json_files:
|
||||
print(f"未找到标注文件,pattern: {glob_pattern}")
|
||||
return 1
|
||||
|
||||
os.makedirs(args.output_dir, exist_ok=True)
|
||||
|
||||
category_counter = Counter()
|
||||
type_counter = Counter()
|
||||
file_object_counter = Counter()
|
||||
category_samples = {}
|
||||
parse_errors = []
|
||||
|
||||
total_objects = 0
|
||||
total_valid_camera_mea = 0
|
||||
|
||||
for json_path in json_files:
|
||||
if '019b4bbf-a702-7c8d-abee-ec1ec17e66e9' in json_path or '019b4c4f-83c5-7aea-b364-cdaf93be8759' in json_path:
|
||||
continue
|
||||
try:
|
||||
with open(json_path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
except Exception as exc:
|
||||
parse_errors.append({"file": json_path, "error": str(exc)})
|
||||
continue
|
||||
|
||||
objects = data.get("asso_list", [])
|
||||
file_object_counter[json_path] = len(objects)
|
||||
|
||||
for index, obj in enumerate(objects):
|
||||
total_objects += 1
|
||||
camera_mea = obj.get("camera_mea")
|
||||
if not isinstance(camera_mea, dict):
|
||||
category = "<missing_camera_mea>"
|
||||
raw_cls = None
|
||||
else:
|
||||
total_valid_camera_mea += 1
|
||||
raw_cls = camera_mea.get("cls")
|
||||
category = canonicalize_cls(raw_cls)
|
||||
|
||||
category_counter[category] += 1
|
||||
type_counter[type(raw_cls).__name__] += 1
|
||||
|
||||
if category not in category_samples:
|
||||
category_samples[category] = []
|
||||
|
||||
if len(category_samples[category]) < args.sample_per_category:
|
||||
category_samples[category].append(
|
||||
{
|
||||
"file": json_path,
|
||||
"index_in_asso_list": index,
|
||||
"raw_cls": raw_cls,
|
||||
"camera_mea_keys": sorted(camera_mea.keys()) if isinstance(camera_mea, dict) else [],
|
||||
}
|
||||
)
|
||||
|
||||
sorted_categories = [category for category, _ in category_counter.most_common()]
|
||||
mapping_template = build_mapping_template(sorted_categories)
|
||||
|
||||
stats_report = {
|
||||
"glob_pattern": glob_pattern,
|
||||
"total_json_files": len(json_files),
|
||||
"total_objects": total_objects,
|
||||
"total_valid_camera_mea": total_valid_camera_mea,
|
||||
"category_value_types": dict(type_counter.most_common()),
|
||||
"category_counts": [
|
||||
{"category": category, "count": count}
|
||||
for category, count in category_counter.most_common()
|
||||
],
|
||||
"category_samples": category_samples,
|
||||
"parse_errors": parse_errors,
|
||||
}
|
||||
|
||||
report_path = os.path.join(args.output_dir, "category_stats.json")
|
||||
mapping_path = os.path.join(args.output_dir, "category_mapping_template.json")
|
||||
|
||||
with open(report_path, "w", encoding="utf-8") as f:
|
||||
json.dump(stats_report, f, ensure_ascii=False, indent=2)
|
||||
|
||||
with open(mapping_path, "w", encoding="utf-8") as f:
|
||||
json.dump(mapping_template, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f"统计完成,JSON 文件数: {len(json_files)}")
|
||||
print(f"目标总数: {total_objects}")
|
||||
print(f"有效 camera_mea 数: {total_valid_camera_mea}")
|
||||
print("类别统计结果:")
|
||||
for category, count in category_counter.most_common():
|
||||
print(f" {category}: {count}")
|
||||
print("cls 字段类型统计:")
|
||||
for value_type, count in type_counter.most_common():
|
||||
print(f" {value_type}: {count}")
|
||||
if parse_errors:
|
||||
print(f"解析失败文件数: {len(parse_errors)}")
|
||||
print(f"详细统计已写入: {report_path}")
|
||||
print(f"映射模板已写入: {mapping_path}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
1458
tools/convert_gt_to_label/visualize_labels.py
Executable file
1458
tools/convert_gt_to_label/visualize_labels.py
Executable file
File diff suppressed because it is too large
Load Diff
323
tools/convert_merge_tracking_bundle/USAGE.md
Executable file
323
tools/convert_merge_tracking_bundle/USAGE.md
Executable file
@@ -0,0 +1,323 @@
|
||||
# convert_merge_tracking_bundle
|
||||
|
||||
## 目录说明
|
||||
|
||||
这个目录整理了 `convert_merge_tracking.py` 及其运行依赖,并额外包含了 `make_jl.py`,方便在独立目录中完成以下两步:
|
||||
|
||||
1. 把 `merge_tracking.json` 转成 `ObjectPerceptionObjectList` 的 protobuf 三件套。
|
||||
2. 再把三件套转成便于查看的 JSON 文本输出。
|
||||
|
||||
目录中的关键文件:
|
||||
|
||||
- `convert_merge_tracking.py`:把 `merge_tracking.json` 转成 protobuf 输出。
|
||||
- `make_jl.py`:把 `ObjectPerceptionObjectList.data.json` 对应的数据转成 JSON 文本。
|
||||
- `proto_reader/reader.py`:按 `data.json + index.json + bin` 的组合方式读取二进制消息。
|
||||
- `pyproto/*.py`:protobuf 生成代码,提供 `ObjectList`、`Object` 等消息定义。
|
||||
- `requirements.txt`:这个独立目录所需的最小第三方 Python 依赖。
|
||||
|
||||
## 环境准备
|
||||
|
||||
建议使用 Python 3。
|
||||
|
||||
安装依赖:
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
`requirements.txt` 中只有两个最小外部依赖:
|
||||
|
||||
- `protobuf>=3.6.1,<4`:用于加载当前目录中的 `*_pb2.py` 生成代码。
|
||||
- `absl-py>=0.7`:`proto_reader/reader.py` 中使用了 `absl.logging`。
|
||||
|
||||
之所以把 `protobuf` 限制在 `<4`,是因为当前打包的 `pyproto/*.py` 是旧风格生成代码,和 4.x 及以上版本可能不兼容。
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1) 把 merge_tracking.json 转成 protobuf 三件套
|
||||
|
||||
```bash
|
||||
python3 convert_merge_tracking.py /path/to/merge_tracking.json -o ./out
|
||||
```
|
||||
|
||||
如果需要强制指定相机 ID:
|
||||
|
||||
```bash
|
||||
python3 convert_merge_tracking.py /path/to/merge_tracking.json -o ./out --cam-id 4
|
||||
```
|
||||
|
||||
执行后会在输出目录生成:
|
||||
|
||||
- `ObjectPerceptionObjectList.bin`
|
||||
- `ObjectPerceptionObjectList.index.json`
|
||||
- `ObjectPerceptionObjectList.data.json`
|
||||
|
||||
这三个文件的关系如下:
|
||||
|
||||
- `*.bin`:连续写入的 protobuf 二进制消息。
|
||||
- `*.index.json`:记录每一帧消息在 `*.bin` 里的 `offset` 和 `size`。
|
||||
- `*.data.json`:入口配置文件,告诉读取程序去哪里找 `bin` 和 `index`。
|
||||
|
||||
### 2) 把 protobuf 三件套转成 JSON 文本
|
||||
|
||||
```bash
|
||||
python3 make_jl.py ./out/ObjectPerceptionObjectList.data.json -o ./out/ObjectPerceptionObjectList.jl
|
||||
```
|
||||
|
||||
如果不加 `-o`,结果会直接输出到终端:
|
||||
|
||||
```bash
|
||||
python3 make_jl.py ./out/ObjectPerceptionObjectList.data.json
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 虽然脚本命名和注释中把输出叫做 `jl` 或 `JsonLines`,但当前实现会给每条记录加缩进,所以每条记录可能占多行。
|
||||
- 因此它更准确地说是“逐条追加的 JSON 文本文件”,而不是严格意义上的单行 JSONL。
|
||||
|
||||
## 脚本功能说明
|
||||
|
||||
### convert_merge_tracking.py
|
||||
|
||||
功能:
|
||||
|
||||
- 读取 `merge_tracking.json`。
|
||||
- 把每一帧的 `detections` 转成 `ObjectPerceptionObjectList` protobuf。
|
||||
- 按帧顺序写入二进制文件,并生成配套索引文件和入口描述文件。
|
||||
|
||||
适用场景:
|
||||
|
||||
- 需要把检测结果接入已有的 `ObjectPerceptionObjectList` 数据链路。
|
||||
- 需要生成能被 `make_jl.py` 继续消费的中间数据。
|
||||
|
||||
### make_jl.py
|
||||
|
||||
功能:
|
||||
|
||||
- 读取 `ObjectPerceptionObjectList.data.json`。
|
||||
- 根据 `index.json` 和 `bin` 逐条取出 protobuf 消息。
|
||||
- 将每条 `ObjectList` 转成 JSON 文本并输出到文件或标准输出。
|
||||
|
||||
适用场景:
|
||||
|
||||
- 想快速查看 protobuf 内容。
|
||||
- 想把二进制结果转成更易读、更方便后处理的 JSON 文本。
|
||||
|
||||
### proto_reader/reader.py
|
||||
|
||||
功能:
|
||||
|
||||
- 解析 `data.json` 中记录的文件位置。
|
||||
- 读取 `index.json`。
|
||||
- 按 `offset + size` 从 `bin` 中精确提取每一条 protobuf 消息的原始字节。
|
||||
|
||||
它是 `make_jl.py` 的底层读取器。
|
||||
|
||||
## 每个脚本的实现逻辑
|
||||
|
||||
### convert_merge_tracking.py 的实现逻辑
|
||||
|
||||
#### 1. 加载 protobuf 定义
|
||||
|
||||
脚本启动时会把当前目录下的 `pyproto` 加入 `sys.path`,然后导入:
|
||||
|
||||
- `object_pb2`
|
||||
- `geometry_pb2`
|
||||
- `camera_pb2`
|
||||
|
||||
其中真正核心的是 `object_pb2`,它定义了 `Object` 和 `ObjectList`。
|
||||
|
||||
#### 2. 定义映射表
|
||||
|
||||
脚本内置了三类映射:
|
||||
|
||||
- `CLASS_ID_MAP`:把输入里的 `class_id` 映射到 `ObjectType` 枚举。
|
||||
- `ANCHOR_MAP`:把 anchor 字符串映射到 `AnchorPtInfo` 枚举。
|
||||
- `FACE_CLS_MAP`:把 `face_cls` 映射到 `VehiclePose` 枚举。
|
||||
|
||||
这些映射决定了输入字段如何落到 protobuf 枚举值上。
|
||||
|
||||
#### 3. 从输入帧中提取 frame_id 和 cam_id
|
||||
|
||||
`parse_image_name()` 会优先从 `image_name` 中解析:
|
||||
|
||||
- 帧号 `frame_id`
|
||||
- 相机号 `cam_id`
|
||||
|
||||
同时脚本也支持:
|
||||
|
||||
- 从 detection 的 `frame_id` 或 `frameId` 中覆盖帧号。
|
||||
- 用 `--cam-id` 强制覆盖相机号。
|
||||
|
||||
#### 4. 把单个 detection 的公共字段填入 protobuf
|
||||
|
||||
`populate_object_fields()` 负责把一个 detection 的常用字段写入目标 `Object`,包括:
|
||||
|
||||
- 类别和枚举映射
|
||||
- `track_id`
|
||||
- `frame_id`
|
||||
- `timestamp`
|
||||
- `lane_assignment`
|
||||
- 图像框信息 `bbox`
|
||||
- 世界坐标下的 `pos / size / yaw`
|
||||
- `face_cls` 对应的姿态
|
||||
- 可选的 `model_3d` 信息
|
||||
|
||||
这个函数是整个转换逻辑的公共字段装配器。
|
||||
|
||||
#### 5. 构建 Mono3D 量测组件
|
||||
|
||||
`build_mono_measure_component()` 会从 `object_3d_ego` 中取出:
|
||||
|
||||
- `x, y, z`
|
||||
- `l, h, w`
|
||||
- `yaw`
|
||||
|
||||
再构造一个 `measure_type = kMeasureMono3D` 的 `Object`,作为量测组件挂到上层对象里。
|
||||
|
||||
#### 6. 构建最终 Object 的层次结构
|
||||
|
||||
`build_object()` 会生成两层核心对象,并形成嵌套关系:
|
||||
|
||||
- `source_obj`:表示单目来源对象,包含图像框、模型 3D、Mono3D 量测等信息。
|
||||
- `obj`:最终输出对象,把 `source_obj` 作为 `key_components` 挂进去。
|
||||
|
||||
如果 `object_3d_ego` 存在,还会额外挂入一个更底层的 Mono3D 量测 `Object`。
|
||||
|
||||
因此最终会形成一种层次化组织:
|
||||
|
||||
- 最终目标对象
|
||||
- 来源单目对象
|
||||
- Mono3D 量测对象
|
||||
|
||||
#### 7. 按帧生成 ObjectList
|
||||
|
||||
`build_object_list()` 对单帧做处理:
|
||||
|
||||
- 创建一个 `ObjectList`
|
||||
- 设置帧号和相机号
|
||||
- 遍历该帧所有 `detections`
|
||||
- 调用 `build_object()` 把每个 detection 追加到 `ObjectList.list`
|
||||
|
||||
如果第一条 detection 里带了 `version`,也会写到 `ObjectList.version`。
|
||||
|
||||
#### 8. 序列化并生成三件套文件
|
||||
|
||||
`convert()` 会遍历所有帧:
|
||||
|
||||
- 把每帧 `ObjectList` 序列化成字节串
|
||||
- 依次写入 `ObjectPerceptionObjectList.bin`
|
||||
- 记录每条消息的 `frame_id / offset / size`
|
||||
- 最后生成 `ObjectPerceptionObjectList.index.json`
|
||||
- 再生成 `ObjectPerceptionObjectList.data.json`
|
||||
|
||||
这样输出结果既能高效存储,也能被后续工具顺序读取。
|
||||
|
||||
### make_jl.py 的实现逻辑
|
||||
|
||||
#### 1. 解析命令行参数
|
||||
|
||||
脚本接收:
|
||||
|
||||
- 位置参数 `data_json`
|
||||
- 可选参数 `-o / --output`
|
||||
|
||||
其中 `data_json` 就是 `ObjectPerceptionObjectList.data.json`。
|
||||
|
||||
#### 2. 通过 Reader 定位真实数据文件
|
||||
|
||||
`make_jsonl()` 内部先创建:
|
||||
|
||||
```python
|
||||
reader = pb_reader.Reader(data_json_path)
|
||||
```
|
||||
|
||||
`Reader` 会:
|
||||
|
||||
- 读取 `data.json`
|
||||
- 找到其中声明的 `index` 和 `data`
|
||||
- 拼出真实的 `index.json` 和 `bin` 路径
|
||||
|
||||
#### 3. 逐条读取 protobuf 消息
|
||||
|
||||
`reader.each()` 会遍历 `index.json` 中的每个索引项:
|
||||
|
||||
- 从索引中拿到 `offset`
|
||||
- 从索引中拿到 `size`
|
||||
- 在 `bin` 中 seek 到对应位置
|
||||
- 读取这一条消息的原始字节
|
||||
|
||||
#### 4. 把字节反序列化成 ObjectList
|
||||
|
||||
读取到的每个 chunk 会被解析成:
|
||||
|
||||
```python
|
||||
object_pb2.ObjectList.FromString(chunk)
|
||||
```
|
||||
|
||||
这样就把原始二进制恢复为 protobuf 对象。
|
||||
|
||||
#### 5. 转成 JSON 并输出
|
||||
|
||||
随后脚本调用:
|
||||
|
||||
```python
|
||||
json_format.MessageToDict(objs, including_default_value_fields=True)
|
||||
```
|
||||
|
||||
把 protobuf 对象转成 Python 字典,并保留默认值字段。
|
||||
|
||||
最后再用 `json.dumps(..., indent=2)` 输出到:
|
||||
|
||||
- 指定文件 `-o`
|
||||
- 或标准输出
|
||||
|
||||
### proto_reader/reader.py 的实现逻辑
|
||||
|
||||
#### 1. 读取 data.json
|
||||
|
||||
`Reader.__init__()` 会先打开 `data.json`,读取:
|
||||
|
||||
- `index[0]`
|
||||
- `data[0]`
|
||||
|
||||
并把它们解析成相对于 `data.json` 所在目录的真实路径。
|
||||
|
||||
#### 2. 加载 index.json
|
||||
|
||||
接着脚本把 `index.json` 读入内存,保存为 `self.object_index`。
|
||||
|
||||
索引中的每一项通常是:
|
||||
|
||||
```json
|
||||
[frame_id, offset, size]
|
||||
```
|
||||
|
||||
后续真正读取数据时主要使用 `offset` 和 `size`。
|
||||
|
||||
#### 3. 迭代返回原始消息字节
|
||||
|
||||
`each()` 打开二进制文件后,会对每条索引执行:
|
||||
|
||||
- `seek(offset)`
|
||||
- `read(size)`
|
||||
- `yield bts`
|
||||
|
||||
因此上层调用方可以把它当作一个“按条产出 protobuf 消息”的生成器。
|
||||
|
||||
## 推荐使用顺序
|
||||
|
||||
```bash
|
||||
python3 convert_merge_tracking.py /path/to/merge_tracking.json -o ./out
|
||||
python3 make_jl.py ./out/ObjectPerceptionObjectList.data.json -o ./out/ObjectPerceptionObjectList.jl
|
||||
```
|
||||
|
||||
如果你只想验证转换是否成功,优先检查:
|
||||
|
||||
- `./out/ObjectPerceptionObjectList.data.json`
|
||||
- `./out/ObjectPerceptionObjectList.index.json`
|
||||
- `./out/ObjectPerceptionObjectList.bin`
|
||||
|
||||
如果你想看可读结果,再检查:
|
||||
|
||||
- `./out/ObjectPerceptionObjectList.jl`
|
||||
574
tools/convert_merge_tracking_bundle/convert_merge_tracking.py
Executable file
574
tools/convert_merge_tracking_bundle/convert_merge_tracking.py
Executable file
@@ -0,0 +1,574 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Convert merge_tracking.json to ObjectPerceptionObjectList protobuf format.
|
||||
|
||||
Produces three files that can be consumed by make_jl.py:
|
||||
- ObjectPerceptionObjectList.data.json
|
||||
- ObjectPerceptionObjectList.bin
|
||||
- ObjectPerceptionObjectList.index.json
|
||||
|
||||
Usage:
|
||||
python convert_merge_tracking.py merge_tracking.json [-o output_dir] [--cam-id N]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), "pyproto"))
|
||||
import object_pb2_new as object_pb2
|
||||
import geometry_pb2
|
||||
import camera_pb2
|
||||
|
||||
# mono3d_ground.yaml class_map numeric ids -> canonical class names.
|
||||
CLASS_ID_TO_NAME = {
|
||||
0: "car",
|
||||
1: "suv",
|
||||
2: "pickup",
|
||||
3: "medium_car",
|
||||
4: "van",
|
||||
5: "bus",
|
||||
6: "truck",
|
||||
7: "special_vehicle",
|
||||
8: "unknown",
|
||||
9: "pedestrian",
|
||||
10: "bicyclist",
|
||||
11: "bicycle",
|
||||
12: "tricycle",
|
||||
13: "traffic_sign",
|
||||
14: "wheel",
|
||||
15: "plate",
|
||||
16: "face",
|
||||
17: "car_fake",
|
||||
18: "bicyclist_fake",
|
||||
19: "pedestrian_fake",
|
||||
}
|
||||
|
||||
# Fake detector categories are carried by ObjectCategory so they do not overwrite
|
||||
# the attribute-origin VehicleClass / PedCls semantics.
|
||||
CAR_FAKE_CATEGORY = getattr(object_pb2.Object, "kCategoryCarFake", object_pb2.Object.kCategoryCar)
|
||||
PEDESTRIAN_FAKE_CATEGORY = getattr(object_pb2.Object, "kCategoryPedestrianFake", object_pb2.Object.kCategoryPedestrian)
|
||||
CYCLIST_FAKE_CATEGORY = getattr(object_pb2.Object, "kCategoryCyclistFake", object_pb2.Object.kCategoryCyclist)
|
||||
|
||||
if CAR_FAKE_CATEGORY == object_pb2.Object.kCategoryCar:
|
||||
print(
|
||||
"[WARN] object_pb2_new.py does not expose kCategoryCarFake/kCategoryPedestrianFake/kCategoryCyclistFake yet. "
|
||||
"Detector-origin fake categories will currently fall back to legacy non-fake ObjectCategory values until "
|
||||
"the protobuf Python bindings are regenerated from object.proto.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
# Canonical / legacy class names -> (ObjectType, ObjectCategory).
|
||||
CLASS_NAME_TO_PROTO = {
|
||||
"car": (object_pb2.Object.kVehicle, object_pb2.Object.kCategoryCar),
|
||||
"car_fake": (object_pb2.Object.kVehicle, CAR_FAKE_CATEGORY),
|
||||
"suv": (object_pb2.Object.kVehicle, object_pb2.Object.kCategorySuv),
|
||||
"pickup": (object_pb2.Object.kVehicle, object_pb2.Object.kCategoryPickup),
|
||||
"medium_car": (object_pb2.Object.kVehicle, object_pb2.Object.kCategoryMediumCar),
|
||||
"van": (object_pb2.Object.kVehicle, object_pb2.Object.kCategoryVan),
|
||||
"bus": (object_pb2.Object.kVehicle, object_pb2.Object.kCategoryBus),
|
||||
"truck": (object_pb2.Object.kVehicle, object_pb2.Object.kCategoryTruck),
|
||||
"tanker": (object_pb2.Object.kVehicle, object_pb2.Object.kCategoryTruck),
|
||||
"large_truck": (object_pb2.Object.kVehicle, object_pb2.Object.kCategoryTruck),
|
||||
"construction_vehicle": (object_pb2.Object.kVehicle, object_pb2.Object.kCategoryTruck),
|
||||
"special_vehicle": (object_pb2.Object.kVehicle, object_pb2.Object.kCategorySpecialVehicle),
|
||||
"unknown": (object_pb2.Object.kVehicle, object_pb2.Object.kCategoryUnknownVehicle),
|
||||
"pedestrian": (object_pb2.Object.kPed, object_pb2.Object.kCategoryPedestrian),
|
||||
"pedestrian_fake": (object_pb2.Object.kPed, PEDESTRIAN_FAKE_CATEGORY),
|
||||
"bicyclist": (object_pb2.Object.kCyclist, object_pb2.Object.kCategoryCyclist),
|
||||
"bicyclist_fake": (object_pb2.Object.kCyclist, CYCLIST_FAKE_CATEGORY),
|
||||
"motorcyclist": (object_pb2.Object.kCyclist, object_pb2.Object.kCategoryCyclist),
|
||||
"bicycle": (object_pb2.Object.kBike, object_pb2.Object.kCategoryBike),
|
||||
"motorcycle": (object_pb2.Object.kBike, object_pb2.Object.kCategoryBike),
|
||||
"tricycle": (object_pb2.Object.kThreeWheeledVehicle, object_pb2.Object.kCategoryTricycle),
|
||||
"tricyclist": (object_pb2.Object.kThreeWheeledVehicle, object_pb2.Object.kCategoryTricycle),
|
||||
# The new schema keeps traffic-sign category fine-grained in ObjectCategory only.
|
||||
"traffic_sign": (object_pb2.Object.kSmallTrafficSign, object_pb2.Object.kCategoryTrafficSign),
|
||||
"wheel": (object_pb2.Object.kVehicleWheel, object_pb2.Object.kCategoryVehicleWheel),
|
||||
"plate": (object_pb2.Object.kVehiclePlate, object_pb2.Object.kCategoryLicensePlate),
|
||||
"face": (object_pb2.Object.kPedHead, object_pb2.Object.kCategoryHead),
|
||||
# Legacy names from the pre-2026 class table.
|
||||
"vehicle": (object_pb2.Object.kVehicle, object_pb2.Object.kCategoryNone),
|
||||
"rider": (object_pb2.Object.kCyclist, object_pb2.Object.kCategoryCyclist),
|
||||
"roadblock": (object_pb2.Object.kRoadBarrier, object_pb2.Object.kCategoryNone),
|
||||
"head": (object_pb2.Object.kPedHead, object_pb2.Object.kCategoryHead),
|
||||
"tsr": (object_pb2.Object.kSmallTrafficSign, object_pb2.Object.kCategoryTrafficSign),
|
||||
"guideboard": (object_pb2.Object.kBigTrafficSign, object_pb2.Object.kCategoryTrafficSign),
|
||||
"tl_border": (object_pb2.Object.kTrafficLight, object_pb2.Object.kCategoryNone),
|
||||
"tl_wick": (object_pb2.Object.kTrafficLightBulb, object_pb2.Object.kCategoryNone),
|
||||
"tl_num": (object_pb2.Object.kTrafficLightDigit, object_pb2.Object.kCategoryNone),
|
||||
}
|
||||
|
||||
# anchor string → AnchorPtInfo enum value
|
||||
ANCHOR_MAP = {
|
||||
"kMonocular3DRear": 22, # AnchorPtInfo.kMonocular3DRear
|
||||
"kMonocular3DFront": 21, # AnchorPtInfo.kMonocular3DFront
|
||||
"kMonocular3DCenter": 25, # AnchorPtInfo.kMonocular3DCenter
|
||||
"kMonocular3DLeft": 23, # AnchorPtInfo.kMonocular3DLeft
|
||||
"kMonocular3DRight": 24, # AnchorPtInfo.kMonocular3DRight
|
||||
}
|
||||
|
||||
# face_cls → VehiclePose enum value
|
||||
FACE_CLS_MAP = {
|
||||
"tail": 2, # kMidTail
|
||||
"head": 5, # kMidHead
|
||||
"side": 14, # kSide
|
||||
"none": 0, # kInvalid
|
||||
}
|
||||
|
||||
MEASURE_MONO_3D = object_pb2.Object.kMeasureMono3D
|
||||
VEHICLE_HIT_TYPES = {
|
||||
object_pb2.Object.kVehicle,
|
||||
object_pb2.Object.kThreeWheeledVehicle,
|
||||
}
|
||||
VEHICLE_CLASS_MIN = object_pb2.Object.kNegative
|
||||
VEHICLE_CLASS_MAX = object_pb2.Object.kFakeCar
|
||||
VEHICLE_CLASS_UNKNOWN = object_pb2.Object.kVehicleUnknown
|
||||
|
||||
|
||||
def parse_numeric_value(value):
|
||||
"""Convert JSON scalar strings to int / float when possible."""
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, (int, float)):
|
||||
return value
|
||||
|
||||
value_str = str(value).strip()
|
||||
if not value_str:
|
||||
return None
|
||||
|
||||
try:
|
||||
numeric = float(value_str)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
if numeric.is_integer():
|
||||
return int(numeric)
|
||||
return numeric
|
||||
|
||||
|
||||
def safe_int(value):
|
||||
"""Best-effort integer conversion."""
|
||||
numeric = parse_numeric_value(value)
|
||||
if numeric is None:
|
||||
return None
|
||||
try:
|
||||
return int(numeric)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def safe_float(value):
|
||||
"""Best-effort float conversion."""
|
||||
numeric = parse_numeric_value(value)
|
||||
if numeric is None:
|
||||
return None
|
||||
try:
|
||||
return float(numeric)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def normalize_name_token(value):
|
||||
"""Normalize class/task-like names to a lowercase underscore form."""
|
||||
normalized = re.sub(r"[^a-z0-9]+", "_", str(value or "").strip().lower())
|
||||
return normalized.strip("_")
|
||||
|
||||
|
||||
def parse_image_name(image_name):
|
||||
"""Extract frame_id and cam_id from image_name.
|
||||
|
||||
Example: "000005_camera4_000006_merged" → frame_id=5, cam_id=4
|
||||
"""
|
||||
frame_id = 0
|
||||
cam_id = 0
|
||||
|
||||
# Extract first number segment as frame index.
|
||||
# Fall back to the penultimate tail number for names like:
|
||||
# G1M3_xxx_uuid_000000_289998
|
||||
m = re.match(r"(\d+)", image_name)
|
||||
if m:
|
||||
frame_id = int(m.group(1))
|
||||
else:
|
||||
m = re.search(r"_(\d+)_(\d+)(?:_merged)?$", image_name)
|
||||
if m:
|
||||
frame_id = int(m.group(1))
|
||||
|
||||
# Extract camera number
|
||||
m = re.search(r"camera(\d+)", image_name)
|
||||
if not m:
|
||||
m = re.search(r"G\d+M(\d+)", image_name)
|
||||
if m:
|
||||
cam_id = int(m.group(1))
|
||||
|
||||
return frame_id, cam_id
|
||||
|
||||
|
||||
def get_detection_frame_id(det):
|
||||
"""Read frame id from either the old or new input schema."""
|
||||
frame_id = det.get("frame_id")
|
||||
if frame_id is None:
|
||||
frame_id = det.get("frameId")
|
||||
return frame_id
|
||||
|
||||
|
||||
def get_frame_timestamp(frame, detections):
|
||||
"""Read the frame timestamp from frame-level data or the first detection."""
|
||||
timestamp = safe_int(frame.get("timestamp"))
|
||||
if timestamp is not None:
|
||||
return timestamp
|
||||
|
||||
if not detections:
|
||||
return None
|
||||
return safe_int(detections[0].get("timestamp"))
|
||||
|
||||
|
||||
def get_detection_class_id(det):
|
||||
"""Read class id from old/new tracking or prediction schemas."""
|
||||
for key in ("class_id", "cls_id", "type"):
|
||||
class_id = safe_int(det.get(key))
|
||||
if class_id is not None:
|
||||
return class_id
|
||||
return None
|
||||
|
||||
|
||||
def get_detection_class_name(det):
|
||||
"""Resolve the most reliable class name for mapping."""
|
||||
for key in ("cls_name", "class_name", "type_name"):
|
||||
value = det.get(key)
|
||||
if value is None:
|
||||
continue
|
||||
class_name = str(value).strip().lower()
|
||||
if not class_name:
|
||||
continue
|
||||
if class_name in CLASS_NAME_TO_PROTO:
|
||||
return class_name
|
||||
|
||||
class_id = get_detection_class_id(det)
|
||||
if class_id is not None and class_id in CLASS_ID_TO_NAME:
|
||||
return CLASS_ID_TO_NAME[class_id]
|
||||
return class_name
|
||||
|
||||
class_id = get_detection_class_id(det)
|
||||
if class_id is None:
|
||||
return ""
|
||||
return CLASS_ID_TO_NAME.get(class_id, "")
|
||||
|
||||
|
||||
def is_detector_fake_vehicle_class(class_name):
|
||||
"""Return whether a normalized detector class name denotes a fake vehicle class."""
|
||||
return normalize_name_token(class_name) == "car_fake"
|
||||
|
||||
|
||||
def resolve_proto_class(det):
|
||||
"""Resolve ObjectType / ObjectCategory from one detection."""
|
||||
class_name = get_detection_class_name(det)
|
||||
if is_detector_fake_vehicle_class(class_name):
|
||||
return object_pb2.Object.kVehicle, CAR_FAKE_CATEGORY, class_name
|
||||
hit_type, object_category = CLASS_NAME_TO_PROTO.get(
|
||||
class_name,
|
||||
(object_pb2.Object.kNone, object_pb2.Object.kCategoryNone),
|
||||
)
|
||||
return hit_type, object_category, class_name
|
||||
|
||||
|
||||
def is_valid_vehicle_class(value):
|
||||
"""Check whether a numeric value fits the VehicleClass enum range."""
|
||||
if value is None:
|
||||
return False
|
||||
return VEHICLE_CLASS_MIN <= int(value) <= VEHICLE_CLASS_MAX
|
||||
|
||||
|
||||
def resolve_vehicle_class_from_attribute(det):
|
||||
"""Map raw vehicle attribute outputs into the proto VehicleClass enum."""
|
||||
attribute = det.get("attribute")
|
||||
if not isinstance(attribute, dict):
|
||||
return None
|
||||
|
||||
if normalize_name_token(attribute.get("task")) != "vehicle":
|
||||
return None
|
||||
|
||||
attr_cls = safe_int(attribute.get("attr_cls"))
|
||||
if attr_cls is None:
|
||||
return None
|
||||
|
||||
is_fake = safe_int(attribute.get("is_fake")) or 0
|
||||
if is_fake == 1:
|
||||
return object_pb2.Object.kFakeCar
|
||||
if attr_cls <= 11:
|
||||
return attr_cls
|
||||
if attr_cls == 23:
|
||||
return object_pb2.Object.kSpecialCar
|
||||
return attr_cls + 3
|
||||
|
||||
|
||||
def resolve_vehicle_class(det, hit_type):
|
||||
"""Resolve VehicleClass for vehicle-like detections.
|
||||
|
||||
Prefer the tracked `sub_cls` field because it already encodes the upstream
|
||||
attribute-to-subclass mapping. Fall back to re-deriving the same mapping
|
||||
from the raw `attribute` payload when `sub_cls` is absent or invalid.
|
||||
"""
|
||||
if hit_type not in VEHICLE_HIT_TYPES:
|
||||
return None
|
||||
|
||||
sub_cls = safe_int(det.get("sub_cls"))
|
||||
if is_valid_vehicle_class(sub_cls):
|
||||
return int(sub_cls)
|
||||
|
||||
attr_vehicle_class = resolve_vehicle_class_from_attribute(det)
|
||||
if is_valid_vehicle_class(attr_vehicle_class):
|
||||
return int(attr_vehicle_class)
|
||||
|
||||
return VEHICLE_CLASS_UNKNOWN
|
||||
|
||||
|
||||
def get_detection_bbox(det):
|
||||
"""Read a bbox from either tracking or prediction-style keys."""
|
||||
for key in ("bbox", "box2d"):
|
||||
bbox = det.get(key)
|
||||
if not isinstance(bbox, (list, tuple)) or len(bbox) < 4:
|
||||
continue
|
||||
values = [safe_float(v) for v in bbox[:4]]
|
||||
if all(v is not None for v in values):
|
||||
return [float(v) for v in values]
|
||||
return None
|
||||
|
||||
|
||||
def build_mono_measure_component(det, anchor_str=None):
|
||||
"""Build the Mono3D measurement component attached to a mono object."""
|
||||
obj_3d_ego = det.get("object_3d_ego")
|
||||
if not obj_3d_ego or len(obj_3d_ego) < 7:
|
||||
return None
|
||||
|
||||
x, y, z, l, h, w, yaw = obj_3d_ego[:7]
|
||||
comp = object_pb2.Object()
|
||||
comp.measure_type = MEASURE_MONO_3D
|
||||
|
||||
wi = comp.world_info
|
||||
wi.pos.x = float(x)
|
||||
wi.pos.y = float(y)
|
||||
wi.pos.z = float(z)
|
||||
wi.size.l = float(l)
|
||||
wi.size.h = float(h)
|
||||
wi.size.w = float(w)
|
||||
wi.pose_angle.yaw = float(yaw)
|
||||
wi.measure_type = MEASURE_MONO_3D
|
||||
if anchor_str and anchor_str in ANCHOR_MAP:
|
||||
wi.anchor = ANCHOR_MAP[anchor_str]
|
||||
|
||||
return comp
|
||||
|
||||
|
||||
def populate_object_fields(det, obj, anchor_str=None, cam_id=None,
|
||||
include_image_info=False, include_model_3d=False):
|
||||
"""Populate a protobuf object with the common fields from one detection."""
|
||||
if anchor_str is None:
|
||||
anchor_str = det.get("anchor")
|
||||
|
||||
class_id = get_detection_class_id(det)
|
||||
hit_type, object_category, class_name = resolve_proto_class(det)
|
||||
obj.hit_type = hit_type
|
||||
obj.hit_id = hit_type
|
||||
obj.object_category = object_category
|
||||
if class_name:
|
||||
obj.hit_type_str = class_name
|
||||
|
||||
track_id = det.get("track_id")
|
||||
if track_id is not None:
|
||||
obj.id = int(track_id)
|
||||
|
||||
frame_id = get_detection_frame_id(det)
|
||||
if frame_id is not None:
|
||||
obj.frame_id = int(frame_id)
|
||||
|
||||
timestamp = det.get("timestamp")
|
||||
if timestamp is not None:
|
||||
obj.timestamp = int(timestamp)
|
||||
|
||||
lane_assignment = det.get("lane_assignment")
|
||||
if lane_assignment is not None:
|
||||
obj.lane_assignment.val = int(lane_assignment)
|
||||
|
||||
if include_image_info:
|
||||
bbox = get_detection_bbox(det)
|
||||
if bbox is not None:
|
||||
x1, y1, x2, y2 = bbox
|
||||
obj.image_info.det_rect.x = float(x1)
|
||||
obj.image_info.det_rect.y = float(y1)
|
||||
obj.image_info.det_rect.w = float(x2 - x1)
|
||||
obj.image_info.det_rect.h = float(y2 - y1)
|
||||
if cam_id is not None:
|
||||
obj.image_info.camera_id.id = int(cam_id)
|
||||
|
||||
if anchor_str and anchor_str in ANCHOR_MAP:
|
||||
obj.world_info.anchor = ANCHOR_MAP[anchor_str]
|
||||
|
||||
obj.world_info.id = obj.id
|
||||
obj.world_info.hit_type = hit_type
|
||||
obj.world_info.object_category = object_category
|
||||
vehicle_class = resolve_vehicle_class(det, hit_type)
|
||||
if vehicle_class is not None:
|
||||
obj.world_info.cls.val = int(vehicle_class)
|
||||
obj.world_info.cls_ori.val = int(vehicle_class)
|
||||
elif class_id is not None:
|
||||
obj.world_info.cls.val = int(class_id)
|
||||
obj.world_info.cls_ori.val = int(class_id)
|
||||
|
||||
face_cls = det.get("face_cls")
|
||||
if face_cls is not None:
|
||||
face_str = str(face_cls)
|
||||
if face_str in FACE_CLS_MAP:
|
||||
obj.world_info.pose.val = FACE_CLS_MAP[face_str]
|
||||
|
||||
obj_3d_ego = det.get("object_3d_ego")
|
||||
if obj_3d_ego and len(obj_3d_ego) >= 7:
|
||||
x, y, z, l, h, w, yaw = obj_3d_ego[:7]
|
||||
wi = obj.world_info
|
||||
wi.pos.x = float(x)
|
||||
wi.pos.y = float(y)
|
||||
wi.pos.z = float(z)
|
||||
wi.size.l = float(l)
|
||||
wi.size.h = float(h)
|
||||
wi.size.w = float(w)
|
||||
wi.pose_angle.yaw = float(yaw)
|
||||
|
||||
if include_model_3d:
|
||||
obj_3d = det.get("object_3d")
|
||||
if obj_3d and len(obj_3d) >= 7:
|
||||
x, y, z, l, h, w, yaw = obj_3d[:7]
|
||||
m3d = obj.world_info.monocular_3d.model_3d_pos
|
||||
m3d.x3d = float(x)
|
||||
m3d.y3d = float(y)
|
||||
m3d.z3d = float(z)
|
||||
m3d.heading = float(yaw)
|
||||
if anchor_str and anchor_str in ANCHOR_MAP:
|
||||
obj.world_info.monocular_3d.anchor = ANCHOR_MAP[anchor_str]
|
||||
|
||||
|
||||
|
||||
def build_object(det, cam_id=None):
|
||||
"""Build a final Object protobuf with source-object and Mono3D nesting."""
|
||||
anchor_str = det.get("anchor")
|
||||
|
||||
source_obj = object_pb2.Object()
|
||||
populate_object_fields(
|
||||
det,
|
||||
source_obj,
|
||||
anchor_str=anchor_str,
|
||||
cam_id=cam_id,
|
||||
include_image_info=True,
|
||||
include_model_3d=True,
|
||||
)
|
||||
source_obj.measure_type = MEASURE_MONO_3D
|
||||
source_obj.world_info.measure_type = MEASURE_MONO_3D
|
||||
|
||||
mono_measure = build_mono_measure_component(det, anchor_str)
|
||||
if mono_measure is not None:
|
||||
source_obj.key_components.add().CopyFrom(mono_measure)
|
||||
|
||||
obj = object_pb2.Object()
|
||||
populate_object_fields(det, obj, anchor_str=anchor_str)
|
||||
obj.key_components.add().CopyFrom(source_obj)
|
||||
return obj
|
||||
|
||||
|
||||
def build_object_list(frame, cam_id_override=None):
|
||||
"""Build an ObjectList protobuf message from a frame dict."""
|
||||
image_name = frame.get("image_name", "")
|
||||
frame_id, cam_id = parse_image_name(image_name)
|
||||
|
||||
if cam_id_override is not None:
|
||||
cam_id = cam_id_override
|
||||
|
||||
obj_list = object_pb2.ObjectList()
|
||||
obj_list.frame_id = frame_id
|
||||
obj_list.cam_id.id = cam_id
|
||||
|
||||
detections = frame.get("detections", [])
|
||||
frame_timestamp = get_frame_timestamp(frame, detections)
|
||||
if frame_timestamp is not None:
|
||||
obj_list.timestamp = int(frame_timestamp)
|
||||
|
||||
# Extract frame_id and version from the first detection.
|
||||
if detections:
|
||||
det_frame_id = get_detection_frame_id(detections[0])
|
||||
if det_frame_id is not None:
|
||||
obj_list.frame_id = int(det_frame_id)
|
||||
det_version = detections[0].get("version")
|
||||
if det_version is not None:
|
||||
obj_list.version = str(det_version)
|
||||
|
||||
for det in detections:
|
||||
obj = build_object(det, cam_id=cam_id)
|
||||
obj_list.list.append(obj)
|
||||
|
||||
return obj_list, frame_id
|
||||
|
||||
|
||||
def convert(input_path, output_dir, cam_id_override=None):
|
||||
"""Convert merge_tracking.json to the three-file protobuf set."""
|
||||
with open(input_path, "r") as f:
|
||||
frames = json.load(f)
|
||||
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
bin_path = os.path.join(output_dir, "ObjectPerceptionObjectList.bin")
|
||||
index_path = os.path.join(output_dir, "ObjectPerceptionObjectList.index.json")
|
||||
data_path = os.path.join(output_dir, "ObjectPerceptionObjectList.data.json")
|
||||
|
||||
index_entries = []
|
||||
offset = 0
|
||||
|
||||
with open(bin_path, "wb") as bin_file:
|
||||
for i, frame in enumerate(frames):
|
||||
obj_list, frame_idx = build_object_list(frame, cam_id_override)
|
||||
serialized = obj_list.SerializeToString()
|
||||
size = len(serialized)
|
||||
|
||||
bin_file.write(serialized)
|
||||
index_entries.append([frame_idx, offset, size])
|
||||
offset += size
|
||||
|
||||
print(f"Convert frame {i} (frame_id={frame_idx}, "
|
||||
f"detections={len(frame.get('detections', []))})",
|
||||
end="\r", file=sys.stderr)
|
||||
|
||||
print(f"\nTotal frames: {len(frames)}", file=sys.stderr)
|
||||
|
||||
# Write index.json
|
||||
with open(index_path, "w") as f:
|
||||
json.dump({"index": index_entries}, f)
|
||||
|
||||
# Write data.json
|
||||
with open(data_path, "w") as f:
|
||||
json.dump({
|
||||
"data": ["ObjectPerceptionObjectList.bin"],
|
||||
"index": ["ObjectPerceptionObjectList.index.json"],
|
||||
"elem_count": len(frames),
|
||||
}, f, indent=2)
|
||||
|
||||
print(f"Output files written to: {output_dir}", file=sys.stderr)
|
||||
print(f" {data_path}", file=sys.stderr)
|
||||
print(f" {bin_path}", file=sys.stderr)
|
||||
print(f" {index_path}", file=sys.stderr)
|
||||
|
||||
return data_path
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(
|
||||
description="Convert merge_tracking.json to ObjectPerceptionObjectList protobuf format")
|
||||
ap.add_argument("input", help="Path to merge_tracking.json")
|
||||
ap.add_argument("-o", "--output-dir", default=".",
|
||||
help="Output directory (default: current directory)")
|
||||
ap.add_argument("--cam-id", type=int, default=None,
|
||||
help="Override camera ID (default: parsed from image_name)")
|
||||
opt = ap.parse_args()
|
||||
|
||||
convert(opt.input, opt.output_dir, opt.cam_id)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
35
tools/convert_merge_tracking_bundle/convert_merge_tracking.sh
Executable file
35
tools/convert_merge_tracking_bundle/convert_merge_tracking.sh
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
|
||||
# python "${PROJECT_ROOT}/tools/convert_merge_tracking_bundle/convert_merge_tracking.py" \
|
||||
# /data1/dongying/Mono3d/G1M3/cases_regular/model_20260228_test/combined_tracking.json \
|
||||
# -o /data1/dongying/Mono3d/G1M3/cases_regular/model_20260228_test/combined_tracking_converted-v2
|
||||
|
||||
# # -----------------------------------------------------------------------
|
||||
# # 配置区:与 track_objects.sh 保持一致
|
||||
# # -----------------------------------------------------------------------
|
||||
RESULTS_ROOT="/data1/dongying/Mono3d/G1M3/cases_regular"
|
||||
MODEL_NAME="model_20260317"
|
||||
OUTPUT_DIR_NAME="combined_tracking_converted"
|
||||
|
||||
echo ""
|
||||
echo "######################################################################"
|
||||
echo "# Batch convert combined_tracking.json for all cases"
|
||||
echo "######################################################################"
|
||||
|
||||
while IFS= read -r -d '' combined_tracking_json; do
|
||||
case_dir="$(dirname "$combined_tracking_json")"
|
||||
output_dir="${case_dir}/${OUTPUT_DIR_NAME}"
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Input : ${combined_tracking_json}"
|
||||
echo "Output : ${output_dir}"
|
||||
echo "=========================================="
|
||||
|
||||
python "${PROJECT_ROOT}/tools/convert_merge_tracking_bundle/convert_merge_tracking.py" \
|
||||
"${combined_tracking_json}" \
|
||||
-o "${output_dir}"
|
||||
done < <(find "${RESULTS_ROOT}/${MODEL_NAME}" -name "combined_tracking.json" -print0 | sort -z)
|
||||
8
tools/convert_merge_tracking_bundle/convert_merge_tracking_case.sh
Executable file
8
tools/convert_merge_tracking_bundle/convert_merge_tracking_case.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
|
||||
python "${PROJECT_ROOT}/tools/convert_merge_tracking_bundle/convert_merge_tracking.py" \
|
||||
/data1/dongying/Mono3d/G1Q3/tmp/20260416115556_results_with_ego/merge.json \
|
||||
-o /data1/dongying/Mono3d/G1Q3/tmp/20260416115556_results_with_ego/objectlist
|
||||
@@ -0,0 +1,376 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
shopt -s nullglob
|
||||
|
||||
# 用法说明:
|
||||
# 适配 tools/temporal_analysis/track_objects_exported_onnx_infer_case.sh
|
||||
# 的输出目录,对每个 case 下的跟踪结果文件执行协议转换。
|
||||
#
|
||||
# 批量模式默认会保留 case 的相对目录结构,例如:
|
||||
# 输入:
|
||||
# {RESULTS_ROOT}/20251115/demo_case/merge.json
|
||||
# 输出:
|
||||
# {OUTPUT_ROOT}/20251115/demo_case/ObjectPerceptionObjectList.data.json
|
||||
# {OUTPUT_ROOT}/20251115/demo_case/ObjectPerceptionObjectList.bin
|
||||
# {OUTPUT_ROOT}/20251115/demo_case/ObjectPerceptionObjectList.index.json
|
||||
# 也支持直接写回每个 case 的子目录,例如:
|
||||
# 输出:
|
||||
# {RESULTS_ROOT}/20251115/demo_case/{OUTPUT_DIR_NAME}/ObjectPerceptionObjectList.data.json
|
||||
# {RESULTS_ROOT}/20251115/demo_case/{OUTPUT_DIR_NAME}/ObjectPerceptionObjectList.bin
|
||||
# {RESULTS_ROOT}/20251115/demo_case/{OUTPUT_DIR_NAME}/ObjectPerceptionObjectList.index.json
|
||||
#
|
||||
# 支持三种模式:
|
||||
# 1) 批量模式(默认):遍历 RESULTS_ROOT 下所有 ${MERGE_JSON_NAME}
|
||||
# bash convert_merge_tracking_exported_onnx_infer_case.sh
|
||||
# 2) 指定根目录模式:第一个参数传跟踪结果根目录,第二个参数可选传保存根目录
|
||||
# bash convert_merge_tracking_exported_onnx_infer_case.sh \
|
||||
# /path/to/model_20260403 \
|
||||
# /path/to/model_20260403_merge_converted
|
||||
# 3) 单 case / 单文件模式:第一个参数传 case 目录、case_dir/predictions 或 merge.json
|
||||
# bash convert_merge_tracking_exported_onnx_infer_case.sh /path/to/one_case
|
||||
# bash convert_merge_tracking_exported_onnx_infer_case.sh /path/to/one_case/predictions
|
||||
# bash convert_merge_tracking_exported_onnx_infer_case.sh /path/to/one_case/merge.json
|
||||
#
|
||||
# 环境变量:
|
||||
# RESULTS_ROOT 默认输入根目录,和 track_objects_exported_onnx_infer_case.sh 保持一致
|
||||
# CASE_LIST_FILE 可选,批量模式下的 case 清单文件;每行一个 case 目录或 merge json 路径
|
||||
# OUTPUT_ROOT 批量模式输出根目录;OUTPUT_LAYOUT=parallel_root 时生效
|
||||
# OUTPUT_LAYOUT 批量模式输出布局:
|
||||
# parallel_root: 输出到独立根目录并保留 case 相对路径
|
||||
# case_subdir : 输出到每个 case 的 {OUTPUT_DIR_NAME}/ 子目录
|
||||
# OUTPUT_DIR_NAME 单 case / case_subdir 模式默认输出目录名(默认: merge_converted)
|
||||
# MERGE_JSON_NAME 要转换的跟踪结果文件名(默认: combined_tracking.json)
|
||||
# PYTHON_BIN Python 解释器路径
|
||||
# CAM_ID 可选,透传给 convert_merge_tracking.py 的 --cam-id
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
|
||||
PYTHON_BIN="${PYTHON_BIN:-/deeplearning_team/ydong/dongying/miniconda/envs/dev/bin/python}"
|
||||
RESULTS_ROOT="${RESULTS_ROOT:-/data1/dongying/Mono3d/G1Q3/model_inference/KPI/OP_KPI_TRAFFIC_SCENARIO/model_20260423-raw_no_edge}"
|
||||
CASE_LIST_FILE="${CASE_LIST_FILE:-}"
|
||||
OUTPUT_ROOT="${OUTPUT_ROOT:-}"
|
||||
OUTPUT_LAYOUT="${OUTPUT_LAYOUT:-parallel_root}"
|
||||
OUTPUT_DIR_NAME="${OUTPUT_DIR_NAME:-merge_converted}"
|
||||
MERGE_JSON_NAME="${MERGE_JSON_NAME:-combined_tracking.json}"
|
||||
CAM_ID="${CAM_ID:-}"
|
||||
CONVERT_PARALLEL_JOBS="${CONVERT_PARALLEL_JOBS:-1}"
|
||||
|
||||
show_usage() {
|
||||
cat <<EOF
|
||||
Usage:
|
||||
bash $(basename "${BASH_SOURCE[0]}") [TARGET_PATH] [OUTPUT_PATH]
|
||||
|
||||
Examples:
|
||||
bash $(basename "${BASH_SOURCE[0]}")
|
||||
bash $(basename "${BASH_SOURCE[0]}") /path/to/model_20260403
|
||||
bash $(basename "${BASH_SOURCE[0]}") /path/to/model_20260403 /path/to/model_20260403_merge_converted
|
||||
OUTPUT_ROOT=/path/to/objectlist_root \\
|
||||
bash $(basename "${BASH_SOURCE[0]}") /path/to/model_20260403
|
||||
OUTPUT_LAYOUT=case_subdir OUTPUT_DIR_NAME=objectlist \\
|
||||
bash $(basename "${BASH_SOURCE[0]}") /path/to/model_20260403
|
||||
bash $(basename "${BASH_SOURCE[0]}") /path/to/one_case
|
||||
bash $(basename "${BASH_SOURCE[0]}") /path/to/one_case /path/to/output_case_dir
|
||||
bash $(basename "${BASH_SOURCE[0]}") /path/to/one_case/combined_tracking.json /path/to/output_case_dir
|
||||
EOF
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "######################################################################"
|
||||
echo "# Batch convert ${MERGE_JSON_NAME} for tracked inference outputs"
|
||||
echo "######################################################################"
|
||||
|
||||
CAM_ID_ARGS=()
|
||||
if [[ -n "${CAM_ID}" ]]; then
|
||||
CAM_ID_ARGS+=(--cam-id "${CAM_ID}")
|
||||
fi
|
||||
|
||||
is_case_dir() {
|
||||
local dir_path="$1"
|
||||
[[ -d "${dir_path}" ]] && [[ -f "${dir_path}/${MERGE_JSON_NAME}" ]]
|
||||
}
|
||||
|
||||
is_predictions_dir() {
|
||||
local dir_path="$1"
|
||||
[[ -d "${dir_path}" ]] || return 1
|
||||
is_case_dir "$(dirname "${dir_path}")"
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
if [[ -f "${target_path}" ]] && [[ "$(basename "${target_path}")" == "${MERGE_JSON_NAME}" ]]; then
|
||||
dirname "${target_path}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
emit_batch_merge_json_paths() {
|
||||
local batch_root="$1"
|
||||
|
||||
if [[ -n "${CASE_LIST_FILE}" ]]; then
|
||||
emit_batch_merge_json_paths_from_list "${batch_root}" "${CASE_LIST_FILE}"
|
||||
return
|
||||
fi
|
||||
|
||||
find "${batch_root}" -type f -name "${MERGE_JSON_NAME}" -print0 | sort -z
|
||||
}
|
||||
|
||||
emit_batch_merge_json_paths_from_list() {
|
||||
local batch_root="${1%/}"
|
||||
local list_file="$2"
|
||||
local list_entry
|
||||
local resolved_path
|
||||
local merge_json
|
||||
|
||||
if [[ ! -f "${list_file}" ]]; then
|
||||
echo "[ERROR] CASE_LIST_FILE not found: ${list_file}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
while IFS= read -r list_entry || [[ -n "${list_entry}" ]]; do
|
||||
list_entry="${list_entry%%$'\r'}"
|
||||
[[ -n "${list_entry}" ]] || continue
|
||||
[[ "${list_entry}" == \#* ]] && continue
|
||||
|
||||
if [[ "${list_entry}" == /* ]]; then
|
||||
resolved_path="${list_entry}"
|
||||
else
|
||||
resolved_path="${batch_root}/${list_entry}"
|
||||
fi
|
||||
|
||||
if [[ -d "${resolved_path}" ]]; then
|
||||
merge_json="${resolved_path%/}/${MERGE_JSON_NAME}"
|
||||
else
|
||||
merge_json="${resolved_path}"
|
||||
fi
|
||||
|
||||
if [[ ! -f "${merge_json}" ]]; then
|
||||
echo "[WARN] Skip missing merge json from CASE_LIST_FILE: ${resolved_path}" >&2
|
||||
continue
|
||||
fi
|
||||
|
||||
printf '%s\0' "${merge_json}"
|
||||
done < "${list_file}"
|
||||
}
|
||||
|
||||
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[@]}")
|
||||
}
|
||||
|
||||
default_batch_output_root() {
|
||||
local batch_root="${1%/}"
|
||||
local parent_dir
|
||||
local base_name
|
||||
|
||||
parent_dir="$(dirname "${batch_root}")"
|
||||
base_name="$(basename "${batch_root}")"
|
||||
printf '%s/%s_merge_converted\n' "${parent_dir}" "${base_name}"
|
||||
}
|
||||
|
||||
run_convert() {
|
||||
local merge_json="$1"
|
||||
local output_dir="$2"
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Input : ${merge_json}"
|
||||
echo "Output : ${output_dir}"
|
||||
echo "=========================================="
|
||||
|
||||
"${PYTHON_BIN}" "${PROJECT_ROOT}/tools/convert_merge_tracking_bundle/convert_merge_tracking.py" \
|
||||
"${merge_json}" \
|
||||
-o "${output_dir}" \
|
||||
"${CAM_ID_ARGS[@]}"
|
||||
}
|
||||
|
||||
run_single_case() {
|
||||
local case_dir="$1"
|
||||
local output_dir="$2"
|
||||
local merge_json="${case_dir}/${MERGE_JSON_NAME}"
|
||||
|
||||
if [[ ! -f "${merge_json}" ]]; then
|
||||
echo "[ERROR] ${MERGE_JSON_NAME} not found in case directory: ${case_dir}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Single-case mode: ${case_dir}"
|
||||
run_convert "${merge_json}" "${output_dir}"
|
||||
}
|
||||
|
||||
run_batch_root() {
|
||||
local batch_root="${1%/}"
|
||||
local output_root="${2:-}"
|
||||
local output_layout="${3:-parallel_root}"
|
||||
local parallel_jobs="${CONVERT_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 [[ -n "${CASE_LIST_FILE}" ]]; then
|
||||
echo "Case-list file : ${CASE_LIST_FILE}"
|
||||
fi
|
||||
case "${output_layout}" in
|
||||
parallel_root)
|
||||
if [[ -z "${output_root}" ]]; then
|
||||
output_root="$(default_batch_output_root "${batch_root}")"
|
||||
fi
|
||||
output_root="${output_root%/}"
|
||||
echo "Output-layout : ${output_layout}"
|
||||
echo "Output-root : ${output_root}"
|
||||
;;
|
||||
case_subdir)
|
||||
echo "Output-layout : ${output_layout}"
|
||||
echo "Output-dir-name : ${OUTPUT_DIR_NAME}"
|
||||
if [[ -n "${output_root}" ]]; then
|
||||
echo "Output-root : ${output_root} (ignored in case_subdir mode)"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "[ERROR] Unsupported OUTPUT_LAYOUT: ${output_layout}" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if ! [[ "${parallel_jobs}" =~ ^[0-9]+$ ]] || [[ "${parallel_jobs}" -lt 1 ]]; then
|
||||
echo "[ERROR] CONVERT_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 '' merge_json; do
|
||||
local case_dir
|
||||
local output_dir
|
||||
|
||||
case_dir="$(dirname "${merge_json}")"
|
||||
((total_cases+=1))
|
||||
|
||||
case "${output_layout}" in
|
||||
parallel_root)
|
||||
if [[ "${case_dir}" == "${batch_root}" ]]; then
|
||||
output_dir="${output_root}"
|
||||
else
|
||||
output_dir="${output_root}/${case_dir#"${batch_root}/"}"
|
||||
fi
|
||||
;;
|
||||
case_subdir)
|
||||
output_dir="${case_dir}/${OUTPUT_DIR_NAME}"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "${parallel_jobs}" == "1" ]]; then
|
||||
if run_convert "${merge_json}" "${output_dir}"; then
|
||||
((success_cases+=1))
|
||||
else
|
||||
((failed_cases+=1))
|
||||
printf '[FAIL] case=%s output=%s\n' "${case_dir}" "${output_dir}" >&2
|
||||
fi
|
||||
else
|
||||
(
|
||||
if run_convert "${merge_json}" "${output_dir}"; then
|
||||
printf 'success\t%s\t%s\n' "${case_dir}" "${output_dir}" > "${status_dir}/$(printf '%05d' "${total_cases}").status"
|
||||
else
|
||||
printf 'failed\t%s\t%s\n' "${case_dir}" "${output_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_merge_json_paths "${batch_root}")
|
||||
|
||||
if [[ "${parallel_jobs}" != "1" ]]; then
|
||||
local pid
|
||||
local status_file
|
||||
local status
|
||||
local case_dir
|
||||
local output_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 output_dir < "${status_file}"
|
||||
if [[ "${status}" == "success" ]]; then
|
||||
((success_cases+=1))
|
||||
else
|
||||
((failed_cases+=1))
|
||||
printf '[FAIL] case=%s output=%s\n' "${case_dir}" "${output_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 ${MERGE_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 [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
show_usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
TARGET_PATH="${1:-${RESULTS_ROOT}}"
|
||||
OUTPUT_PATH_ARG="${2:-}"
|
||||
|
||||
if [[ ! -e "${TARGET_PATH}" ]]; then
|
||||
echo "Error: target path does not exist: ${TARGET_PATH}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if RESOLVED_CASE_DIR="$(resolve_case_dir "${TARGET_PATH}")"; then
|
||||
SINGLE_OUTPUT_DIR="${OUTPUT_PATH_ARG:-${OUTPUT_ROOT:-${RESOLVED_CASE_DIR}/${OUTPUT_DIR_NAME}}}"
|
||||
run_single_case "${RESOLVED_CASE_DIR}" "${SINGLE_OUTPUT_DIR}"
|
||||
elif [[ -d "${TARGET_PATH}" ]]; then
|
||||
BATCH_OUTPUT_ROOT="${OUTPUT_PATH_ARG:-${OUTPUT_ROOT:-}}"
|
||||
run_batch_root "${TARGET_PATH}" "${BATCH_OUTPUT_ROOT}" "${OUTPUT_LAYOUT}"
|
||||
else
|
||||
echo "Error: unsupported target path: ${TARGET_PATH}" >&2
|
||||
show_usage >&2
|
||||
exit 1
|
||||
fi
|
||||
58
tools/convert_merge_tracking_bundle/make_jl.py
Executable file
58
tools/convert_merge_tracking_bundle/make_jl.py
Executable file
@@ -0,0 +1,58 @@
|
||||
# Copyright (c) 2022 the ObjectPerceptionTool authors. All right reserved.
|
||||
#
|
||||
# This is a script to convert *.index.json and its data into JsonLines.
|
||||
#
|
||||
# This requires protobuf and yj_pyproto_versions. See:
|
||||
#
|
||||
# https://git.minieye.tech/pcview/yj_pyproto_versions
|
||||
#
|
||||
# Usage:
|
||||
#
|
||||
# make_jl.py xx.data.json -d xx.bin -o xx.jl
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
from google.protobuf import json_format
|
||||
|
||||
import proto_reader.reader as pb_reader
|
||||
|
||||
sys.path.append(os.path.join(sys.path[0], "pyproto"))
|
||||
import object_pb2_new as object_pb2
|
||||
|
||||
|
||||
def make_jsonl(data_json_path, writer):
|
||||
reader = pb_reader.Reader(data_json_path)
|
||||
|
||||
i = 0
|
||||
for chunk in reader.each():
|
||||
# objs = json.loads(chunk)
|
||||
objs = object_pb2.ObjectList.FromString(chunk)
|
||||
objs = json_format.MessageToDict(objs, including_default_value_fields=True)
|
||||
|
||||
writer.write(json.dumps(objs, indent=2) + "\n")
|
||||
print("Convert message", i, end="\r", file=sys.stderr)
|
||||
i += 1
|
||||
|
||||
print("\ndone", file=sys.stderr)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("data_json")
|
||||
ap.add_argument("-o", "--output")
|
||||
opt = ap.parse_args()
|
||||
|
||||
try:
|
||||
if opt.output is None:
|
||||
make_jsonl(opt.data_json, sys.stdout)
|
||||
else:
|
||||
with open(opt.output, "w") as writer:
|
||||
make_jsonl(opt.data_json, writer)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
|
||||
main()
|
||||
691
tools/convert_merge_tracking_bundle/object.proto
Executable file
691
tools/convert_merge_tracking_bundle/object.proto
Executable file
@@ -0,0 +1,691 @@
|
||||
syntax = "proto3";
|
||||
import "google/protobuf/any.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
import "geometry.proto";
|
||||
import "data_source.proto";
|
||||
import "calib_param.proto";
|
||||
import "vehicle_signal.proto";
|
||||
import "vehicle_signal_v2.proto";
|
||||
import "command_signal.proto";
|
||||
import "object_warning.proto";
|
||||
import "camera.proto";
|
||||
import "scene.proto";
|
||||
import "tsr_define.proto";
|
||||
|
||||
// VERSION
|
||||
// 0.6
|
||||
|
||||
package perception;
|
||||
|
||||
message Object {
|
||||
// 目标检测分类
|
||||
enum ObjectType {
|
||||
kNone = 0;
|
||||
kVehicle = 1; // 车辆
|
||||
kPed = 2; // 行人
|
||||
kBike = 3; // 二轮车
|
||||
kCone = 4; // 锥桶
|
||||
|
||||
kVehicleWheel = 5; // 车轮
|
||||
kVehiclePlate = 6; // 车牌
|
||||
kPedHead = 7; // 人头
|
||||
kCyclist = 8; // 骑行者
|
||||
kWarningTriangle = 9; // 三角警告牌
|
||||
kSmallTrafficSign = 10; // 小标牌
|
||||
kBigTrafficSign = 11; // 大标牌
|
||||
kTrafficLight = 12; // 交通信号灯
|
||||
kAnimals = 13; // 动物
|
||||
kVehicleLight = 14; // 车灯
|
||||
kStreetLamp = 15; // 路灯
|
||||
kRoadBarrier = 16; // 水马
|
||||
kTrafficLightBulb = 17; // 交通信号灯的灯芯
|
||||
kTrafficLightDigit = 18; // 交通信号灯的数字
|
||||
kObstacle = 19; // 障碍物
|
||||
kThreeWheeledVehicle = 20;// 三轮车
|
||||
kObjectTypeNum = 24;
|
||||
}
|
||||
|
||||
enum ObjectCategory {
|
||||
kCategoryNone = 0;
|
||||
|
||||
kCategoryCar = 1; // 小轿车
|
||||
kCategorySuv = 2; // SUV
|
||||
kCategoryPickup = 3; // 皮卡
|
||||
kCategoryMediumCar = 4; // 中型车
|
||||
kCategoryVan = 5; // 面包车
|
||||
kCategoryBus = 6; // 客车
|
||||
kCategoryTruck = 7; // 卡车/罐车/大型货车/工程车
|
||||
kCategorySpecialVehicle = 8; // 特种车
|
||||
kCategoryTricycle = 9; // 三轮车/三轮骑行者
|
||||
kCategoryUnknownVehicle = 10; // 未知车辆
|
||||
|
||||
kCategoryPedestrian = 30; // 行人
|
||||
kCategoryBike = 31; // 自行车/摩托车
|
||||
kCategoryCyclist = 32; // 自行车骑行者/摩托车骑行者
|
||||
|
||||
kCategoryTrafficSign = 40; // 交通标志牌
|
||||
|
||||
kCategoryVehicleWheel = 90; // 车轮
|
||||
kCategoryLicensePlate = 91; // 车牌
|
||||
kCategoryHead = 92; // 人头
|
||||
}
|
||||
|
||||
// 车辆姿态分类
|
||||
enum VehiclePose {
|
||||
kInvalid = 0; // 背景
|
||||
kLeftTail = 1; // 左侧同向车
|
||||
kMidTail = 2; // 中间同向车
|
||||
kRightTail = 3; // 右侧同向车
|
||||
kLeftHead = 4; // 左侧对向车
|
||||
kMidHead = 5; // 中间对向车
|
||||
kRightHead = 6; // 右侧对向车
|
||||
kLeftSide = 7; // 朝左横向车
|
||||
kRightSide = 8; // 朝右横向车
|
||||
kLeftCutIn = 9; // 左侧切入
|
||||
kRightCutIn = 10; // 右侧切入
|
||||
kLeftCutOut = 11; // 左侧切出
|
||||
kRightCutOut = 12; // 右侧切出
|
||||
kOccluded = 13; // 车头遮挡/车尾遮挡/横向遮挡 (deprecated)
|
||||
kSide = 14; // 横向车(不知道朝左,还是朝右)
|
||||
kOccludedTail = 15; // 车尾遮挡
|
||||
kOccludedHead = 16; // 车头遮挡
|
||||
kOccludedSide = 17; // 横向遮挡
|
||||
kVehicleOtherPose = 18; // 其他(切车、横向车、遮挡车)
|
||||
kUnknownPose = 20; // unknown pose
|
||||
}
|
||||
|
||||
enum OccludedType {
|
||||
kWithoutOccluded = 0; // 无遮挡
|
||||
kPartOccluded = 1; // 部分遮挡
|
||||
kLeftOccluded = 2; // 左遮挡
|
||||
kRightOccluded = 4; // 右遮挡
|
||||
kTopOccluded = 8; // 上遮挡
|
||||
kBottomOccluded = 16; // 底部遮挡
|
||||
}
|
||||
|
||||
// 行人姿态分类
|
||||
enum PedPose {
|
||||
kPedInvalidPose = 0; // 背景
|
||||
kPedForward = 1; // 朝前
|
||||
kPedBackward = 4; // 朝后
|
||||
kPedLeft = 7; // 朝左
|
||||
kPedRight = 10; // 朝右
|
||||
kPedUnknownPose = 20; // 未知朝向
|
||||
}
|
||||
|
||||
enum PedCls {
|
||||
kPedInvalidCls = 0; // 背景
|
||||
kPedFull = 1; // 完整行人
|
||||
kPedOccluded = 2; // 遮挡行人
|
||||
kPedCyclist = 3; // 骑车行人
|
||||
kPedAdvert = 4; // 广告行人
|
||||
kPedFakeFull = 5; // 伪装直立行人
|
||||
kPedFakeCyclist = 6; // 伪装骑行者
|
||||
kPedUnknownCls = 20; // 未知类别
|
||||
}
|
||||
|
||||
// 锚点信息
|
||||
enum AnchorPtInfo {
|
||||
kVehicleHead = 0;
|
||||
kVehicleTail = 1;
|
||||
kVehicleFrontWheel = 2; // deprecated
|
||||
kVehicleRearWheel = 3; // deprecated
|
||||
kVehicleSomeWheel = 4;
|
||||
kDetectBottomCenter = 5;
|
||||
kVehicleLeftFrontWheel = 6;
|
||||
kVehicleLeftRearWheel = 7;
|
||||
kVehicleRightFrontWheel = 8;
|
||||
kVehicleRightRearWheel = 9;
|
||||
kMonocular3DFront = 21;
|
||||
kMonocular3DRear = 22;
|
||||
kMonocular3DLeft = 23;
|
||||
kMonocular3DRight = 24;
|
||||
kMonocular3DCenter = 25;
|
||||
}
|
||||
|
||||
// 车辆测距方式
|
||||
enum MeasureType {
|
||||
kVehicleMeasureHeadReg = 0;
|
||||
kVehicleMeasureTailReg = 1;
|
||||
kVehicleMeasureDetect = 2;
|
||||
kVehicleMeasureWheel = 3;
|
||||
kVehicleMeasurePlate = 4;
|
||||
kVehicleMeasureHeadProjection = 5;
|
||||
kVehicleMeasureTailProjection = 6;
|
||||
kVehicleMeasureHeadWidth = 7;
|
||||
kVehicleMeasureTailWidth = 8;
|
||||
kPedMeasureBox = 10; // 行人测距方式
|
||||
kPedMeasureProjection = 11;
|
||||
|
||||
// add 2024/09/20
|
||||
kVehicleMeasureLaneWidth = 9; // 基于车道宽度
|
||||
kVehicleMeasureDetHeight = 12; // 基于检测框高度
|
||||
kVehicleMeasureDetWidth = 13; // 基于检测框宽度
|
||||
kVehicleMeasureRegHeight = 14; // 基于回归框高度
|
||||
kVehicleMeasureRegWidth = 15; // 基于回归框宽度
|
||||
|
||||
kMeasureMono3D = 16; // 模型测距方式
|
||||
kMeasureBev = 17;
|
||||
kMeasureFusion = 20; // 融合测距
|
||||
}
|
||||
|
||||
// 车型细分类
|
||||
enum VehicleClass {
|
||||
kNegative = 0; // 背景
|
||||
kBus = 1; // 大巴
|
||||
kCar = 2; // 小轿车,suv
|
||||
kMiniBus = 3; // 面包车
|
||||
kBucketTruck = 4; // 斗卡
|
||||
kContainerTruck = 5; // 箱卡
|
||||
kTricycle = 6; // 三轮车
|
||||
kTanker = 7; // 油罐车,晒水车(车身带有圆形,椭圆形,半圆形的罐)
|
||||
kCementTankTruck = 8; // 水泥罐车
|
||||
kPickup = 9; // 皮卡
|
||||
kSedimentTruck = 10; // 渣土车
|
||||
kIveco = 11; // 依维柯
|
||||
kSpecialCar = 12; // 异型车
|
||||
kCityAuto = 13; // 市政车
|
||||
kVehicleUnknown = 14; // 未知车辆
|
||||
kWrecker = 15; // 小型拖车,清障车
|
||||
kFlatTransporter = 16; // 大型平板拖车
|
||||
kTractorHead = 17; // 卡车车头,牵引车车头
|
||||
kSpecialFlatTransporter = 18; // 大型特种拖车(相对于大型平板拖车会多挡板)
|
||||
kCarCarrier = 19; // 轿运车
|
||||
|
||||
// 盖布车(车身上的货物全部被布盖着,同时对车辆尾部进行了遮挡)
|
||||
kTruckWithTrap = 20;
|
||||
kEngineeringVehicle = 21; // 工程车
|
||||
kSweeper = 22; // 扫地车
|
||||
kRoadServiceCar = 23; // 道路维修车
|
||||
kSanitationTruck = 24; // 环卫车
|
||||
kGarbageTruck = 25; // 垃圾车
|
||||
kFakeCar = 26; // 伪装小车
|
||||
}
|
||||
|
||||
enum WheelClass {
|
||||
kWheelUnknown = 0; // 车轮方向未知
|
||||
kWheelFront = 1; // 前车轮
|
||||
kWheelRear = 2; // 后车轮
|
||||
}
|
||||
|
||||
enum TrafficLightColor { // 交通灯颜色 cls
|
||||
kTLCBackground = 0; // 背景
|
||||
kTLCRed = 1; // 红灯
|
||||
kTLCGreen = 2; // 绿灯
|
||||
kTLCYellow = 3; // 黄灯
|
||||
kTLCUnknown = 8; // 未知
|
||||
}
|
||||
|
||||
enum TrafficLightShape { // 交通灯形状 pose
|
||||
kTLSBackground = 0; // 背景
|
||||
kTLSUp = 1; // 上
|
||||
kTLSDown = 2; // 下
|
||||
kTLSLeft = 3; // 左
|
||||
kTLSRight = 4; // 右
|
||||
kTLSCircle = 5; // 圆
|
||||
kTLSTurnAround = 6; // 调头
|
||||
kTLSDigit = 7; // 数字
|
||||
kTLSPerson = 8; // 人
|
||||
kTLSBicycle = 9; // 自行车
|
||||
kTLSUnknown = 16; // 未知
|
||||
}
|
||||
|
||||
enum VehicleLightClass { // 车灯分类
|
||||
kVLUnknown = 0;
|
||||
kVLHeadLight = 1; // 车头灯
|
||||
kVLTailLight = 2; // 车尾灯
|
||||
kVLBrakeLight = 4; // 刹车等
|
||||
kVLTurnLeftLight = 8; // 左变道灯
|
||||
kVLTurnRightLight = 16; // 右变道灯
|
||||
}
|
||||
|
||||
enum VehicleLightStatus { // 车灯状态
|
||||
kVLSUnknown = 0;
|
||||
kVLSOn = 1; // 开
|
||||
kVLSOff = 2; // 关(指示灯全灭)
|
||||
kVLSFlash = 3; // 闪烁(deprecated)
|
||||
kVLSLeftTurnLightOn = 4; // 左转向灯亮
|
||||
kVLSRightTurnLightOn = 8; // 右转向灯亮
|
||||
kVLSBrakeLightOn = 16; // 刹车灯亮
|
||||
kVLSHazardLightOn = 32; // 双闪灯亮
|
||||
}
|
||||
|
||||
// 工况场景判断
|
||||
enum MoveState {
|
||||
kMSUnknown = 0; // 未知
|
||||
kEgoDirectionDriving = 1; // 跟车流方向一致,向前行驶
|
||||
kEgoDirectionStopped = 2; // 跟车流方向一致, 停止
|
||||
kEgoDirectionReversing = 3; // 跟车流方向一致, 横穿
|
||||
kOnComming = 4; // 对向来车
|
||||
kCrossing = 5; // 横穿车辆
|
||||
kStationary = 6; // 静止
|
||||
kTurning = 7; // 转弯
|
||||
}
|
||||
|
||||
// 目标位置状态 -> 2024/09/20
|
||||
enum PositionState {
|
||||
kPosUnknown = 0; // 未知
|
||||
kPosShortDistance = 1; // 近距离
|
||||
kPosMediumDistance = 2; // 中距离
|
||||
kPosLongDistance = 3; // 远距离
|
||||
}
|
||||
|
||||
message ImageInfo {
|
||||
perception.common.Rect2f det_rect = 1; // 检测框
|
||||
perception.common.Rect2f reg_rect = 2; // 回归框(尾部框)
|
||||
perception.common.Rect2f track_rect = 3; // 跟踪框
|
||||
perception.common.Rect2f smooth_rect = 4; // 光流跟踪框
|
||||
perception.common.Rect2f plate_rect = 5; // 车牌框
|
||||
repeated perception.common.Rect2f wheel_list = 6; // 车轮踪框
|
||||
repeated perception.common.Point2f reg_pt_list = 7; // 回归点列表
|
||||
perception.common.Rect2f bike_rect = 8; // 二轮车框
|
||||
|
||||
perception.common.Box3D box = 10; // 3dbox
|
||||
minieye.CamID camera_id = 11; // 对应那个摄像头
|
||||
|
||||
perception.common.Rect2f det_rc_refined = 15; // 经过refine(补框)后的检测框
|
||||
perception.common.Rect2f reg_rc_refined = 16; // 经过refine(补框)后的回归框
|
||||
perception.common.Rect2f det_rc_refined_vel = 17; // 经过refine(补框)后的检测框的速速
|
||||
reserved 18;
|
||||
GroundContactLines ground_contact_lines = 19; // 车辆四个面的接地线,图像侧按 (u, v, z) 组织
|
||||
}
|
||||
|
||||
message GroundContactLines {
|
||||
perception.common.Point3fList front = 1; // 前面接地线,固定 5 个点
|
||||
perception.common.Point3fList rear = 2; // 后面接地线,固定 5 个点
|
||||
perception.common.Point3fList left = 3; // 左面接地线,固定 5 个点
|
||||
perception.common.Point3fList right = 4; // 右面接地线,固定 5 个点
|
||||
}
|
||||
|
||||
enum VehicleRigidAnchor {
|
||||
kVRARear = 0;
|
||||
kVRARearWheel = 1;
|
||||
kVRAHeadWheel = 2;
|
||||
kVRAHead = 3;
|
||||
kVRAMaxNum = 4;
|
||||
}
|
||||
|
||||
enum ObjImportanceType {
|
||||
kNormal = 0; // 普通的目标
|
||||
kCrucial = 1; // 决定性的目标,cipv, cipp
|
||||
kInterested = 2; // 关注的目标
|
||||
}
|
||||
|
||||
message VehicleRigid {
|
||||
// 长度方向,刚体量测细节
|
||||
repeated perception.common.Float meas_along_length =
|
||||
1; // kVRAMaxNum * kVRAMaxNum
|
||||
perception.common.Float w = 2; // width
|
||||
perception.common.Float h = 3; // height
|
||||
}
|
||||
|
||||
enum TrackStatus {
|
||||
kTSUnknown = 0;
|
||||
kTSFirstDetectd = 1; // 第一次检测
|
||||
kTSTracking = 2; // 稳定跟踪 (只针对关键车)
|
||||
kTSPredict = 3; // 预测
|
||||
}
|
||||
|
||||
enum SelectLevelStatus {
|
||||
kSLOriginDet = 0; // 原始检测
|
||||
kSLDetAfterInnerROINMS = 1; // 经过ROI内NMS后的检测
|
||||
kSLDetAfterCrossROINMS = 2; // 经过ROI间NMS后的检测
|
||||
kSLReservered = 3; // 保留字段
|
||||
kSLCandidateDet = 4; // 候选目标 (所有检测出来的目标都是候选目标)
|
||||
kSLConcerned = 5; // 需要重点关注的目标
|
||||
kSLCrucial = 6; // 决定性的目标 (cipv, cipp)
|
||||
}
|
||||
|
||||
enum LaneDirection { // 车道朝向
|
||||
kSameDirectionLane = 0; // 同向车道
|
||||
kOppositeDirectionLane = 1; // 对向车道
|
||||
kObjDirectionUnknown = 2; // 方向unknown
|
||||
}
|
||||
|
||||
enum PropertyType {
|
||||
kPropertyVehicleRearLamp = 0;
|
||||
kPropertyInvalidity = 1;
|
||||
kPropertyOcclusion = 2;
|
||||
kPropertyLeftOccluded = 3; // 左遮挡
|
||||
kPropertyRightOccluded = 4; // 右遮挡
|
||||
kPropertyBottomOccluded = 5; // 底部遮挡
|
||||
kPropertyTopOccluded = 6; // 上部遮挡
|
||||
}
|
||||
|
||||
enum PropertyInvalidity {
|
||||
kReservedInvalidity = 0;
|
||||
kVelocityInvalidity = 1;
|
||||
kPositionInvalidity = 2;
|
||||
kHeadingInvalidity = 4;
|
||||
}
|
||||
|
||||
enum ObstacleType {
|
||||
kObstacleBackground = 0; // 背景
|
||||
kObstacleCone = 1; // 锥桶
|
||||
kObstacleCrashBarrel = 2; // 防撞桶
|
||||
kObstacleWarningTriangle = 3; // 三角警告牌
|
||||
kObstacleCylindrical = 4; // 柱形石墩
|
||||
kObstacleSphericalstone = 5; // 球形石墩
|
||||
kObstaclePlasticpole = 6; // 塑料地桩
|
||||
kObstacleBumpingpost = 7; // 铁地桩
|
||||
kObstacleWaterbarrier = 8; // 水马
|
||||
kObstacleCarton = 9; // 纸箱子
|
||||
kObstacleTyre = 10; // 轮胎
|
||||
kObstaclePlasticpoleBumpingpost = 11; // 地桩(塑料地桩、铁地桩)
|
||||
kObstacleOther = 32; // 其它
|
||||
}
|
||||
|
||||
enum Mode {
|
||||
kNoneMode = 0;
|
||||
kPilotMode = 1; // 行车
|
||||
kParkingMode = 2; // 泊车
|
||||
kCamera1Mono3d = 4;// camera1使用mono3d
|
||||
}
|
||||
|
||||
// 法规场景具体类型
|
||||
enum SpecificSceneType {
|
||||
kSceneUnknown = 0; // 未知
|
||||
kSceneCNCAP2024SCP = 1; // SCP场景
|
||||
kSceneCNCAP2024CCFT = 2; // CCFT场景
|
||||
kSceneCNCAP2024CCRS = 3; // CCRS场景
|
||||
kSceneCNCAP2024SCPO = 4; // SCPO场景
|
||||
kSceneCNCAP2024CCRH = 5; // CCRH场景
|
||||
kSceneCNCAP2024CPLA = 6; // CPLA场景
|
||||
kSceneCNCAP2024CPLAN = 7; // CPLA_N场景
|
||||
kSceneCNCAP2024CPTALN = 8; // CPTA_LN场景
|
||||
kSceneCNCAP2024CPTALF = 9; // CPTA_LF场景
|
||||
kSceneCNCAP2024CPTARF = 10; // CPTA_RF场景
|
||||
kSceneCNCAP2024CPFAO = 11; // CPFAO场景
|
||||
kSceneCNCAP2024CPFAON = 12; // CPFAO_N场景
|
||||
kSceneCNCAP2024CPNCO = 13; // CPNCO场景
|
||||
kSceneCNCAP2024CBNAO = 14; // CBNAO场景
|
||||
kSceneCNCAP2024CSFAO = 15; // CSFAO场景
|
||||
kSceneCNCAP2024CSTALN = 16; // CSTA_LN场景
|
||||
kSceneCNCAP2024CBLA = 17; // CBLA场景
|
||||
kSceneCNCAP2024CSTARN = 18; // CSTA_RN场景
|
||||
kScene1242CPFA = 19; // 行人从左到右横穿
|
||||
kScene1242CPNA = 20; // 行人从右到左横穿
|
||||
kScene1242CPLA = 21; // 正前方静止行人
|
||||
kScene1242CCRS = 22; // 1242 CCRS场景
|
||||
kScene1242CCRM = 23; // 1242 CCRM场景
|
||||
}
|
||||
//
|
||||
//enum SFObstaclePropertyType {
|
||||
//
|
||||
// SAF_OBS_PRO_NOT_ON_ROAD = (1<<0),【根据freespace判断⽬标是否在道路边界外】
|
||||
// SAF_OBS_PRO_COVERD_BY_OTHERS = (1 << 1),//【⽬标是否被物体遮挡】
|
||||
// SAF_OBS_PRO_OBSERVE_JUMP = (1 << 2),// 【通过图像检测框判断⽬标是否跳变】
|
||||
// SAF_OBS_PRO_OBSERVE_SIDE = (1 << 3),// 【⽬标是否在图像边缘】
|
||||
//通过⻆点、检测框、速度⽐例判断横穿转直⾏,仅骑⻋⼈输出,内部使⽤
|
||||
// SAF_OBS_PRO_BT_TAP_LEVEL_1 = (1 << 4),
|
||||
// SAF_OBS_PRO_BT_TAP_LEVEL_2 = (1 << 5),
|
||||
// SAF_OBS_PRO_BT_TAP_LEVEL_3 = (1 << 6),
|
||||
// SAF_OBS_PRO_PED_ON_MOTOR = (1 << 7),//【可能是三轮⻋上⾏⼈flag,根据IOU进⾏判断】
|
||||
// SAF_OBS_PRO_PED_GROUP = 1 << 8, //【⼈群】
|
||||
// SAF_OBS_PRO_CROSSING_GHOST= 1 << 9,//【⻤探头】
|
||||
//}
|
||||
message PropertyValue {
|
||||
int32 value = 1;
|
||||
float valuef = 2;
|
||||
float confidence = 3;
|
||||
}
|
||||
|
||||
message OperatingCondition {
|
||||
enum RoadSurface {
|
||||
kRoadSurfaceSmooth = 0;
|
||||
kRoadSurfaceBumpy = 1;
|
||||
}
|
||||
enum RoadGradient {
|
||||
kRoadGradientFlatGround = 0;
|
||||
kRoadGradientUphill = 1;
|
||||
kRRoadGradientDownhill = 2;
|
||||
}
|
||||
enum MotionState {
|
||||
kMotionStateNone = 0;
|
||||
kMotionStateStationary = 1;
|
||||
kMotionStateMoving = 2;
|
||||
kMotionStateTurning = 4;
|
||||
kMotionStateStraight = 8;
|
||||
}
|
||||
bool airborne = 1; // 悬空
|
||||
RoadSurface road_surface = 2; // 颠簸
|
||||
RoadGradient road_gradient = 3; // 上下坡
|
||||
uint32 motion_state = 4; // 运动状态
|
||||
}
|
||||
|
||||
message LaneAssignment {
|
||||
perception.common.Int lane_index = 1; // 车道位置(--3 -2 -1 0 1 2 3)
|
||||
LaneDirection direction = 2; // 车道朝向
|
||||
// 是否侵道 ( > 0 - 侵道, < 0 - 未侵道, 其值为侵入的多少, 单位m)
|
||||
perception.common.Float has_cut_lane = 3;
|
||||
|
||||
// 透视图下车尾侵入本车道的比例[-103, 100]
|
||||
// < 0, 侵入当前右车道线,> 0, 侵入当前左车道线, 0 - 无侵入
|
||||
// -101 -> 无车尾回归框
|
||||
// -102 -> 无匹配车道线
|
||||
// -103 -> 既无回归框也无车道线
|
||||
perception.common.Int cut_in_ratio_on_persp = 4;
|
||||
|
||||
// 关联的车道属性
|
||||
repeated perception.common.Int associated_lane_idxs = 5;
|
||||
}
|
||||
|
||||
// 单目3D信息
|
||||
message Monocular3D {
|
||||
// Box3D中心点的像素坐标(xc, yc)
|
||||
perception.common.Point2f ctr_pt_2d = 1;
|
||||
// 深度(z)
|
||||
perception.common.Float z = 2;
|
||||
|
||||
// Box3D(l, w, h)
|
||||
perception.common.Float l = 3;
|
||||
perception.common.Float w = 4;
|
||||
perception.common.Float h = 5;
|
||||
|
||||
// alpha(人眼看的车辆方向)
|
||||
perception.common.Float alpha = 6;
|
||||
|
||||
// 模型原始测距的锚点信息
|
||||
AnchorPtInfo anchor = 7;
|
||||
|
||||
// 模型原始输出的3D位置和朝向信息
|
||||
message Model3DPos {
|
||||
float x3d = 1;
|
||||
float y3d = 2;
|
||||
float z3d = 3;
|
||||
float heading = 4;
|
||||
}
|
||||
Model3DPos model_3d_pos = 8;
|
||||
}
|
||||
|
||||
message WorldInfo {
|
||||
perception.common.XYZ vel = 1; // 速度
|
||||
perception.common.XYZ rel_vel = 2; // 相对速度
|
||||
perception.common.XYZ acc = 3; // 加速度
|
||||
perception.common.XYZ pos = 4; // 位置
|
||||
perception.common.Size3D size = 5; // 长宽高
|
||||
// 与自车的中心夹角 左边缘夹角 右边缘夹角
|
||||
perception.common.Angle3f angle = 6;
|
||||
|
||||
// 基于回归框估计的宽高
|
||||
perception.common.Size3D size_est1 = 7;
|
||||
|
||||
// 基于检测框估计的宽高
|
||||
perception.common.Size3D size_est2 = 8;
|
||||
|
||||
perception.common.Box3D box = 10; // 3dbox
|
||||
|
||||
uint32 id = 11; // 目标id
|
||||
|
||||
// 类型细分类,可能取:
|
||||
//
|
||||
// - VehicleClass
|
||||
// - PedCls
|
||||
// - TrafficLightColor
|
||||
// - tsr.SignType
|
||||
//
|
||||
perception.common.Int cls = 12;
|
||||
|
||||
// 模糊类型。
|
||||
//
|
||||
// 遮挡等情况下,模型无法判断物体具体类型,possible_cls_list
|
||||
// 表示此物体的可能类型。
|
||||
//
|
||||
// - 当此列表为空时,视作仅包含 cls.val。
|
||||
// - 当此列表仅包含 cls.val,表示结果不模糊的。
|
||||
// - 否则,cls.val 的值是无效的(一般设为某种未知类型)。
|
||||
repeated int32 possible_cls_list = 42;
|
||||
|
||||
float val = 13; // 分类值
|
||||
perception.common.Int pose = 14; // 姿态分类 : VehiclePose PedPose TrafficLightShape
|
||||
float ttc = 15; // 碰撞时间
|
||||
float headway = 16; // 时距=距离/自车速度
|
||||
int32 cipv = 17; // 前方目标是否为关键车(CIPV),关键车为1,非关键车为0
|
||||
int32 cipp = 18; // 前方目标是否为关键人(CIPP),关键人为1,非关键人为0
|
||||
perception.common.PoseAngle pose_angle =
|
||||
19; // 姿态角,单位:度,左正右负, ±180
|
||||
perception.common.Int motion_state = 20; // 运动状态,参考MoveState定义
|
||||
AnchorPtInfo anchor = 21; // 测距对应的锚点信息
|
||||
|
||||
perception.common.Point3D pos_var = 22; // pos的方差(deprecated)
|
||||
perception.common.Point3D size_var = 23; // 长宽高的方差(deprecated)
|
||||
|
||||
// (deprecated) pose_angle的方差
|
||||
perception.common.Point3D pose_angle_var = 24;
|
||||
perception.common.XYZ anchor_offset = 25; // 锚点的offset
|
||||
repeated perception.common.XY odom_trace = 26; // 历史轨迹
|
||||
|
||||
perception.common.Int pose_ori = 30; // 模型直出的pose
|
||||
perception.common.Int cls_ori = 31; // 模型直出的cls
|
||||
ObjectType hit_type = 32; // 目标检测大类别
|
||||
float confidence = 33; // 综合置信度
|
||||
MeasureType measure_type = 34; // 量测类型
|
||||
TrackStatus track_status = 35; // 跟踪状态
|
||||
LaneAssignment road_assignment = 36; // 目标所在的车道信息
|
||||
int32 life_time = 37; // 生命周期
|
||||
int32 index = 38; // 索引
|
||||
uint32 age = 39; // 目标存活周期
|
||||
|
||||
Monocular3D monocular_3d = 40; // 单目3D信息
|
||||
//障碍物(⾏⼈、骑⻋⼈)属性,包含:是否在道路边界外, 是否被遮挡等,其中pro_flag按照bitmap⽅式SFObstaclePropertyType填充:
|
||||
int32 pro_flag = 41;
|
||||
ObjectCategory object_category = 43; // 检测细分类,保留模型原始类别语义
|
||||
GroundContactLines ground_contact_lines = 44; // 车辆四个面的接地线,转换到车身坐标系后的点集
|
||||
}
|
||||
|
||||
message FusionInfo { // fusion要用到的关联信息
|
||||
// 关联到的各个摄像头的检测框
|
||||
repeated ImageInfo associated_image_infos = 1;
|
||||
// 关联到的各个摄像头的检测框
|
||||
repeated WorldInfo associated_world_infos = 2;
|
||||
repeated float associated_conf_matrix = 3; // 关联置信度 num * num
|
||||
WorldInfo fusion_world_info = 4; // 多视fusion后的结果
|
||||
map<int32, PropertyValue> properties = 5; // <PropertyType, PropertyValue>
|
||||
}
|
||||
|
||||
int32 hit_id = 1; // 目标检测ID (deprecated, 以hit_type为准)
|
||||
ObjectType hit_type = 2; // 目标检测类别enum
|
||||
string hit_type_str = 6; // 目标检测类别string
|
||||
float confidence = 3; // 检测置信度(综合)
|
||||
|
||||
int32 frame_cnt = 4; // frame count
|
||||
int32 life_time = 5; // live us
|
||||
uint64 time_creation = 7; // 障碍物被识别的时间戳(微秒)
|
||||
|
||||
ImageInfo image_info = 8; // 图像信息 (前视1v专用)
|
||||
WorldInfo world_info = 9; // 车辆坐标系下信息 (前视1v专用)
|
||||
|
||||
int32 track_status = 10; // track状态
|
||||
int32 trace_status = 11; // deprecated
|
||||
int32 select_level = 12; // 选择等级 (SelectLevelStatus)
|
||||
|
||||
uint32 id = 15; // object id
|
||||
FusionInfo fusion_info = 16; // fusion需要信息
|
||||
MeasureType measure_type = 17; // 量测类型
|
||||
|
||||
uint64 timestamp = 18; // 当前时刻 timestamp (微秒)
|
||||
uint64 frame_id = 19; // 当前帧 frame id
|
||||
|
||||
perception.common.Int lane_assignment = 21; // 目标所在车道信息(deprecated)
|
||||
// 是否侵道 ( > 0 - 侵道, < 0 - 未侵道, 其值为侵入的多少, 单位m)(deprecated)
|
||||
perception.common.Float has_cut_lane = 22;
|
||||
|
||||
VehicleRigid veh_rigid = 23; // 车辆刚体信息
|
||||
|
||||
perception.common.FloatArray features = 24; // 特征向量
|
||||
|
||||
LaneAssignment road_assignment = 25; // 目标所在车道信息
|
||||
uint32 occluded_mask = 26; // 值域范围(0 | OccludedType[i])
|
||||
google.protobuf.Timestamp timestamp_ts = 27; // 管理面时间
|
||||
google.protobuf.Timestamp tick_ts = 28; // 数据面时间
|
||||
// 遮挡率可以使用该map表示
|
||||
map<int32, PropertyValue> properties = 29; // <PropertyType, PropertyValue>
|
||||
OperatingCondition operating_condition = 30; // 工况
|
||||
SpecificSceneType specific_scene = 31; // 法规场景具体类型
|
||||
uint32 age = 32; // 目标存活周期
|
||||
ObjectCategory object_category = 33; // 检测细分类,保留模型原始类别语义
|
||||
|
||||
repeated Object key_components = 60; // 组件信息
|
||||
}
|
||||
|
||||
// topic : "ObjectPerceptionObjectList"
|
||||
message ObjectList {
|
||||
repeated Object list = 1; // object list
|
||||
|
||||
string version = 3; // 版本号
|
||||
|
||||
uint64 frame_id = 4; // 帧IDs
|
||||
uint64 timestamp = 5; // utc时戳, us
|
||||
|
||||
perception.object.Warning warning = 6; // 告警
|
||||
repeated perception.common.Rect2f roi_list = 7; // 检测ROI列表
|
||||
|
||||
uint64 tick = 9; // tick时戳, us
|
||||
uint64 start_time_us = 10; // 算法流程开始时间
|
||||
uint64 end_time_us = 11; // 算法流程结束时间
|
||||
repeated uint64 profiling_time = 12; // 模块耗时列表
|
||||
repeated Scene scene = 13; // 场景分类(deprecated)
|
||||
SceneList scene_list = 14; // 场景分类
|
||||
|
||||
minieye.DataSource data_source = 15; // 描述数据源 字节数: 4
|
||||
minieye.CalibParam calib_param = 16; // 标定参数
|
||||
minieye.VehicleSignal vehicle_signal = 17; // 车身信号
|
||||
minieye.CameraParam camera_param = 18; // 相机内外参数(多v融合结果不适用)
|
||||
minieye.CamID cam_id = 19; // 摄像头id(cam_id.id(), 等同instance_id)
|
||||
uint32 cam_prj_id = 20; // 用于进行投影变换的id
|
||||
minieye.CommandSignal command_signal = 21; // 触发式信号
|
||||
google.protobuf.Timestamp timestamp_ts = 22; // 管理面时间
|
||||
google.protobuf.Timestamp tick_ts = 23; // 数据面时间
|
||||
|
||||
minieye.VehicleSignalHighFreq vehicle_signal_high_freq = 24; // 车身高频信号
|
||||
minieye.VehicleSignalLowFreq vehicle_signal_low_freq = 25; // 车身低频信号
|
||||
uint32 mode = 26; // 模式
|
||||
repeated uint32 frame_ids = 27; // [前短焦,后,前长焦,往左前看,往左后看,往右前看,往右后看] 不存在的补0
|
||||
uint64 roadmarking_frame_id = 28; // 匹配路面感知的frame_id
|
||||
uint64 roadmarking_tick_ms = 29; // 匹配路面感知的tick(毫秒)
|
||||
google.protobuf.Any debug_details = 30; // debug信息
|
||||
}
|
||||
|
||||
// topic : "ObjectPerceptionVisionObjects"
|
||||
message VisionObjects {
|
||||
string version = 1; // 版本号
|
||||
google.protobuf.Timestamp tick = 2; // 数据面时间
|
||||
google.protobuf.Timestamp timestamp = 3; // 管理面时间
|
||||
repeated Object.FusionInfo fusion_info = 4; // 视觉检测结果
|
||||
// [前短焦,后,前长焦,往左前看,往左后看,往右前看,往右后看]
|
||||
// 不存在的补0
|
||||
repeated uint32 frame_ids = 5; // 各v的frame_id
|
||||
SceneList scene = 6; // 场景分类
|
||||
|
||||
google.protobuf.Timestamp start_tick = 7; // 算法开始时间
|
||||
google.protobuf.Timestamp end_tick = 8; // 算法结束时间
|
||||
uint32 mode = 9; // 模式
|
||||
uint64 roadmarking_frame_id = 10; // 匹配路面感知的frame_id
|
||||
uint64 roadmarking_tick_ms = 11; // 匹配路面感知的tick(毫秒)
|
||||
}
|
||||
|
||||
// topic : "VisionObjectsWarning"
|
||||
// perception.object.Warning
|
||||
|
||||
// topic : "SagWarning"
|
||||
// perception.object.SAGWarning
|
||||
35
tools/convert_merge_tracking_bundle/proto_reader/reader.py
Executable file
35
tools/convert_merge_tracking_bundle/proto_reader/reader.py
Executable file
@@ -0,0 +1,35 @@
|
||||
from google.protobuf import json_format
|
||||
import os
|
||||
import json
|
||||
import sys
|
||||
|
||||
from absl import logging
|
||||
|
||||
|
||||
class Reader:
|
||||
def __init__(self, data_file_path):
|
||||
with open(data_file_path) as reader:
|
||||
data_file_conf = json.load(reader)
|
||||
|
||||
data_file_base_path = os.path.dirname(data_file_path)
|
||||
|
||||
index_file_path = os.path.join(data_file_base_path, data_file_conf["index"][0])
|
||||
binary_file_path = os.path.join(data_file_base_path, data_file_conf["data"][0])
|
||||
|
||||
with open(index_file_path, "r") as object_index_file:
|
||||
self.object_index = json.load(object_index_file)
|
||||
|
||||
self.binary_file_path = binary_file_path
|
||||
|
||||
def each(self):
|
||||
with open(self.binary_file_path, "rb") as object_pb_file:
|
||||
for obj_idx in self.object_index["index"]:
|
||||
offset = obj_idx[1]
|
||||
size = obj_idx[2]
|
||||
|
||||
object_pb_file.seek(offset)
|
||||
bts = object_pb_file.read(size)
|
||||
|
||||
logging.debug("data size %d", size)
|
||||
|
||||
yield bts
|
||||
1075
tools/convert_merge_tracking_bundle/pyproto/calib_param_pb2.py
Executable file
1075
tools/convert_merge_tracking_bundle/pyproto/calib_param_pb2.py
Executable file
File diff suppressed because it is too large
Load Diff
995
tools/convert_merge_tracking_bundle/pyproto/camera_pb2.py
Executable file
995
tools/convert_merge_tracking_bundle/pyproto/camera_pb2.py
Executable file
@@ -0,0 +1,995 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: camera.proto
|
||||
|
||||
from google.protobuf.internal import enum_type_wrapper
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
from google.protobuf import reflection as _reflection
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor.FileDescriptor(
|
||||
name='camera.proto',
|
||||
package='minieye',
|
||||
syntax='proto3',
|
||||
serialized_options=None,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
serialized_pb=b'\n\x0c\x63\x61mera.proto\x12\x07minieye\x1a\x1fgoogle/protobuf/timestamp.proto\"\xe4\x03\n\x17\x43\x61meraFrameExtendedInfo\x12\x0b\n\x03seq\x18\x01 \x01(\r\x12\x12\n\nframe_type\x18\x02 \x01(\r\x12\x11\n\tdata_size\x18\x03 \x01(\x05\x12\r\n\x05width\x18\x04 \x01(\x05\x12\x0e\n\x06height\x18\x05 \x01(\x05\x12\x15\n\rfsync_ads_sec\x18\x06 \x01(\r\x12\x16\n\x0e\x66sync_ads_nsec\x18\x07 \x01(\r\x12\x16\n\x0e\x66sync_gnss_sec\x18\x08 \x01(\r\x12\x17\n\x0f\x66sync_gnss_nsec\x18\t \x01(\r\x12\x19\n\x11\x65xp_start_ads_sec\x18\n \x01(\r\x12\x1a\n\x12\x65xp_start_ads_nsec\x18\x0b \x01(\r\x12\x1a\n\x12\x65xp_start_gnss_sec\x18\x0c \x01(\r\x12\x1b\n\x13\x65xp_start_gnss_nsec\x18\r \x01(\r\x12\x17\n\x0f\x65xp_end_ads_sec\x18\x0e \x01(\r\x12\x18\n\x10\x65xp_end_ads_nsec\x18\x0f \x01(\r\x12\x18\n\x10\x65xp_end_gnss_sec\x18\x10 \x01(\r\x12\x19\n\x11\x65xp_end_gnss_nsec\x18\x11 \x01(\r\x12\x11\n\tshutter_1\x18\x12 \x01(\r\x12\x11\n\tshutter_2\x18\x13 \x01(\r\x12\x18\n\x10image_supplement\x18\x14 \x01(\x0c\"\x8f\x02\n\x0b\x43\x61meraFrame\x12\x11\n\tcamera_id\x18\x01 \x01(\r\x12\x11\n\ttimestamp\x18\x02 \x01(\x04\x12\x0c\n\x04tick\x18\x03 \x01(\x04\x12\x10\n\x08\x66rame_id\x18\x04 \x01(\x04\x12\x18\n\x10image_plane_addr\x18\x05 \x03(\x04\x12\x13\n\x0bimage_width\x18\x06 \x01(\r\x12\x14\n\x0cimage_height\x18\x07 \x01(\r\x12\x0e\n\x06stride\x18\x08 \x01(\r\x12\x12\n\nimage_type\x18\t \x01(\r\x12\x18\n\x10image_supplement\x18\n \x01(\x0c\x12\x37\n\rextended_info\x18\x0b \x01(\x0b\x32 .minieye.CameraFrameExtendedInfo\"E\n\x05\x43\x61mID\x12\x12\n\ncam_direct\x18\x01 \x01(\x05\x12\x1c\n\x03\x66ov\x18\x02 \x01(\x0e\x32\x0f.minieye.CamFov\x12\n\n\x02id\x18\x03 \x01(\x05\"[\n\x0bTransMatrix\x12\x12\n\nvcsgnd2img\x18\x01 \x03(\x02\x12\x12\n\nimg2vcsgnd\x18\x02 \x03(\x02\x12\x11\n\tlocal2img\x18\x03 \x03(\x02\x12\x11\n\timg2local\x18\x04 \x03(\x02\"\xf5\x04\n\x0b\x43\x61meraParam\x12\x0f\n\x07\x66ocal_u\x18\x01 \x01(\x02\x12\x0f\n\x07\x66ocal_v\x18\x02 \x01(\x02\x12\n\n\x02\x63u\x18\x03 \x01(\x02\x12\n\n\x02\x63v\x18\x04 \x01(\x02\x12\x0b\n\x03pos\x18\x05 \x03(\x02\x12\r\n\x05pitch\x18\x06 \x01(\x02\x12\x0b\n\x03yaw\x18\x07 \x01(\x02\x12\x0c\n\x04roll\x18\x08 \x01(\x02\x12\x0b\n\x03\x66ov\x18\t \x01(\x02\x12\x14\n\x0cimage_format\x18\n \x01(\x05\x12\x0f\n\x07isp_ver\x18\x0b \x01(\t\x12\x19\n\x11install_direction\x18\x0c \x01(\x05\x12\'\n\ttrans_mtx\x18\r \x01(\x0b\x32\x14.minieye.TransMatrix\x12+\n\tprj_model\x18\x0e \x01(\x0e\x32\x18.minieye.ProjectionModel\x12\x13\n\x0bimage_width\x18\x0f \x01(\r\x12\x14\n\x0cimage_height\x18\x10 \x01(\r\x12\x16\n\x0e\x64istort_coeffs\x18\x11 \x03(\x01\x12\x11\n\tcamera_id\x18\x12 \x01(\r\x12\x10\n\x08is_valid\x18\x13 \x01(\x08\x12\x14\n\x0c\x63\x61lib_method\x18\x14 \x01(\x05\x12\x17\n\x0f\x63\x61lib_timestamp\x18\x15 \x01(\x04\x12\x0c\n\x04rvec\x18\x16 \x03(\x02\x12\x0c\n\x04tvec\x18\x17 \x03(\x02\x12\t\n\x01s\x18\x18 \x01(\x02\x12\x15\n\rvehicle_width\x18\x19 \x01(\x02\x12\x12\n\nwheel_base\x18\x1a \x01(\x02\x12\x16\n\x0e\x66ront_overhang\x18\x1b \x01(\x02\x12\x15\n\raffine_params\x18\x1c \x03(\x02\x12\x13\n\x0bpoly_coeffs\x18\x1d \x03(\x02\x12\x17\n\x0finv_poly_coeffs\x18\x1e \x03(\x02\x12\n\n\x02sn\x18\x1f \x01(\t\"\xac\x02\n\x12\x43\x61meraEmbeddedInfo\x12\x13\n\x0b\x66rame_count\x18\x01 \x01(\r\x12\x10\n\x08\x65xpo_num\x18\x02 \x01(\r\x12\x12\n\nexpo_ratio\x18\x03 \x03(\r\x12\x14\n\x0cshutter_time\x18\x04 \x03(\r\x12\x15\n\rsensor_a_gain\x18\x05 \x03(\x02\x12\x15\n\rsensor_d_gain\x18\x06 \x03(\x02\x12?\n\x1b\x65xp_start_camera_time_stamp\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12=\n\x19\x65xp_end_camera_time_stamp\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x17\n\x0fis_valid_bitmap\x18@ \x01(\x04*\x80\x01\n\x0bImageFormat\x12\x12\n\x0e\x46ORMAT_UNKNOWN\x10\x00\x12\x08\n\x04GRAY\x10\x01\x12\x08\n\x04YV12\x10\x02\x12\x08\n\x04JPEG\x10\x03\x12\x07\n\x03PNG\x10\x04\x12\x08\n\x04\x43R12\x10\x05\x12\x07\n\x03\x42\x41\x44\x10\x06\x12\x08\n\x04NV12\x10\x07\x12\x08\n\x04NV21\x10\x08\x12\x0f\n\x0b\x42YPASS_ONLY\x10\t*I\n\tCamDirect\x12\x12\n\x0e\x44IRECT_UNKNOWN\x10\x00\x12\t\n\x05\x46RONT\x10\x01\x12\x08\n\x04REAR\x10\x02\x12\x08\n\x04LEFT\x10\x04\x12\t\n\x05RIGHT\x10\x08*2\n\x06\x43\x61mFov\x12\x0f\n\x0bkFovUnknown\x10\x00\x12\n\n\x06kFov30\x10\x01\x12\x0b\n\x07kFov100\x10\x02*m\n\x0fProjectionModel\x12\x15\n\x11PRJ_MODEL_UNKNOWN\x10\x00\x12\x0b\n\x07\x46ISHEYE\x10\x01\x12\x07\n\x03MEI\x10\x02\x12\x0c\n\x08PIN_HOLE\x10\x03\x12\x08\n\x04\x41TAN\x10\x04\x12\x15\n\x11\x44\x41VIDE_SCARAMUZZA\x10\x05\x62\x06proto3'
|
||||
,
|
||||
dependencies=[google_dot_protobuf_dot_timestamp__pb2.DESCRIPTOR,])
|
||||
|
||||
_IMAGEFORMAT = _descriptor.EnumDescriptor(
|
||||
name='ImageFormat',
|
||||
full_name='minieye.ImageFormat',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
values=[
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='FORMAT_UNKNOWN', index=0, number=0,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='GRAY', index=1, number=1,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='YV12', index=2, number=2,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='JPEG', index=3, number=3,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='PNG', index=4, number=4,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='CR12', index=5, number=5,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='BAD', index=6, number=6,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='NV12', index=7, number=7,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='NV21', index=8, number=8,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='BYPASS_ONLY', index=9, number=9,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
containing_type=None,
|
||||
serialized_options=None,
|
||||
serialized_start=1919,
|
||||
serialized_end=2047,
|
||||
)
|
||||
_sym_db.RegisterEnumDescriptor(_IMAGEFORMAT)
|
||||
|
||||
ImageFormat = enum_type_wrapper.EnumTypeWrapper(_IMAGEFORMAT)
|
||||
_CAMDIRECT = _descriptor.EnumDescriptor(
|
||||
name='CamDirect',
|
||||
full_name='minieye.CamDirect',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
values=[
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='DIRECT_UNKNOWN', index=0, number=0,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='FRONT', index=1, number=1,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='REAR', index=2, number=2,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='LEFT', index=3, number=4,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='RIGHT', index=4, number=8,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
containing_type=None,
|
||||
serialized_options=None,
|
||||
serialized_start=2049,
|
||||
serialized_end=2122,
|
||||
)
|
||||
_sym_db.RegisterEnumDescriptor(_CAMDIRECT)
|
||||
|
||||
CamDirect = enum_type_wrapper.EnumTypeWrapper(_CAMDIRECT)
|
||||
_CAMFOV = _descriptor.EnumDescriptor(
|
||||
name='CamFov',
|
||||
full_name='minieye.CamFov',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
values=[
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kFovUnknown', index=0, number=0,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kFov30', index=1, number=1,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kFov100', index=2, number=2,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
containing_type=None,
|
||||
serialized_options=None,
|
||||
serialized_start=2124,
|
||||
serialized_end=2174,
|
||||
)
|
||||
_sym_db.RegisterEnumDescriptor(_CAMFOV)
|
||||
|
||||
CamFov = enum_type_wrapper.EnumTypeWrapper(_CAMFOV)
|
||||
_PROJECTIONMODEL = _descriptor.EnumDescriptor(
|
||||
name='ProjectionModel',
|
||||
full_name='minieye.ProjectionModel',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
values=[
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='PRJ_MODEL_UNKNOWN', index=0, number=0,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='FISHEYE', index=1, number=1,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='MEI', index=2, number=2,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='PIN_HOLE', index=3, number=3,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='ATAN', index=4, number=4,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='DAVIDE_SCARAMUZZA', index=5, number=5,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
containing_type=None,
|
||||
serialized_options=None,
|
||||
serialized_start=2176,
|
||||
serialized_end=2285,
|
||||
)
|
||||
_sym_db.RegisterEnumDescriptor(_PROJECTIONMODEL)
|
||||
|
||||
ProjectionModel = enum_type_wrapper.EnumTypeWrapper(_PROJECTIONMODEL)
|
||||
FORMAT_UNKNOWN = 0
|
||||
GRAY = 1
|
||||
YV12 = 2
|
||||
JPEG = 3
|
||||
PNG = 4
|
||||
CR12 = 5
|
||||
BAD = 6
|
||||
NV12 = 7
|
||||
NV21 = 8
|
||||
BYPASS_ONLY = 9
|
||||
DIRECT_UNKNOWN = 0
|
||||
FRONT = 1
|
||||
REAR = 2
|
||||
LEFT = 4
|
||||
RIGHT = 8
|
||||
kFovUnknown = 0
|
||||
kFov30 = 1
|
||||
kFov100 = 2
|
||||
PRJ_MODEL_UNKNOWN = 0
|
||||
FISHEYE = 1
|
||||
MEI = 2
|
||||
PIN_HOLE = 3
|
||||
ATAN = 4
|
||||
DAVIDE_SCARAMUZZA = 5
|
||||
|
||||
|
||||
|
||||
_CAMERAFRAMEEXTENDEDINFO = _descriptor.Descriptor(
|
||||
name='CameraFrameExtendedInfo',
|
||||
full_name='minieye.CameraFrameExtendedInfo',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='seq', full_name='minieye.CameraFrameExtendedInfo.seq', index=0,
|
||||
number=1, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='frame_type', full_name='minieye.CameraFrameExtendedInfo.frame_type', index=1,
|
||||
number=2, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='data_size', full_name='minieye.CameraFrameExtendedInfo.data_size', index=2,
|
||||
number=3, type=5, cpp_type=1, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='width', full_name='minieye.CameraFrameExtendedInfo.width', index=3,
|
||||
number=4, type=5, cpp_type=1, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='height', full_name='minieye.CameraFrameExtendedInfo.height', index=4,
|
||||
number=5, type=5, cpp_type=1, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='fsync_ads_sec', full_name='minieye.CameraFrameExtendedInfo.fsync_ads_sec', index=5,
|
||||
number=6, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='fsync_ads_nsec', full_name='minieye.CameraFrameExtendedInfo.fsync_ads_nsec', index=6,
|
||||
number=7, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='fsync_gnss_sec', full_name='minieye.CameraFrameExtendedInfo.fsync_gnss_sec', index=7,
|
||||
number=8, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='fsync_gnss_nsec', full_name='minieye.CameraFrameExtendedInfo.fsync_gnss_nsec', index=8,
|
||||
number=9, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='exp_start_ads_sec', full_name='minieye.CameraFrameExtendedInfo.exp_start_ads_sec', index=9,
|
||||
number=10, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='exp_start_ads_nsec', full_name='minieye.CameraFrameExtendedInfo.exp_start_ads_nsec', index=10,
|
||||
number=11, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='exp_start_gnss_sec', full_name='minieye.CameraFrameExtendedInfo.exp_start_gnss_sec', index=11,
|
||||
number=12, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='exp_start_gnss_nsec', full_name='minieye.CameraFrameExtendedInfo.exp_start_gnss_nsec', index=12,
|
||||
number=13, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='exp_end_ads_sec', full_name='minieye.CameraFrameExtendedInfo.exp_end_ads_sec', index=13,
|
||||
number=14, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='exp_end_ads_nsec', full_name='minieye.CameraFrameExtendedInfo.exp_end_ads_nsec', index=14,
|
||||
number=15, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='exp_end_gnss_sec', full_name='minieye.CameraFrameExtendedInfo.exp_end_gnss_sec', index=15,
|
||||
number=16, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='exp_end_gnss_nsec', full_name='minieye.CameraFrameExtendedInfo.exp_end_gnss_nsec', index=16,
|
||||
number=17, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='shutter_1', full_name='minieye.CameraFrameExtendedInfo.shutter_1', index=17,
|
||||
number=18, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='shutter_2', full_name='minieye.CameraFrameExtendedInfo.shutter_2', index=18,
|
||||
number=19, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='image_supplement', full_name='minieye.CameraFrameExtendedInfo.image_supplement', index=19,
|
||||
number=20, type=12, cpp_type=9, label=1,
|
||||
has_default_value=False, default_value=b"",
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
],
|
||||
serialized_options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto3',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=59,
|
||||
serialized_end=543,
|
||||
)
|
||||
|
||||
|
||||
_CAMERAFRAME = _descriptor.Descriptor(
|
||||
name='CameraFrame',
|
||||
full_name='minieye.CameraFrame',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='camera_id', full_name='minieye.CameraFrame.camera_id', index=0,
|
||||
number=1, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='timestamp', full_name='minieye.CameraFrame.timestamp', index=1,
|
||||
number=2, type=4, cpp_type=4, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='tick', full_name='minieye.CameraFrame.tick', index=2,
|
||||
number=3, type=4, cpp_type=4, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='frame_id', full_name='minieye.CameraFrame.frame_id', index=3,
|
||||
number=4, type=4, cpp_type=4, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='image_plane_addr', full_name='minieye.CameraFrame.image_plane_addr', index=4,
|
||||
number=5, type=4, cpp_type=4, label=3,
|
||||
has_default_value=False, default_value=[],
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='image_width', full_name='minieye.CameraFrame.image_width', index=5,
|
||||
number=6, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='image_height', full_name='minieye.CameraFrame.image_height', index=6,
|
||||
number=7, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='stride', full_name='minieye.CameraFrame.stride', index=7,
|
||||
number=8, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='image_type', full_name='minieye.CameraFrame.image_type', index=8,
|
||||
number=9, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='image_supplement', full_name='minieye.CameraFrame.image_supplement', index=9,
|
||||
number=10, type=12, cpp_type=9, label=1,
|
||||
has_default_value=False, default_value=b"",
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='extended_info', full_name='minieye.CameraFrame.extended_info', index=10,
|
||||
number=11, type=11, cpp_type=10, label=1,
|
||||
has_default_value=False, default_value=None,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
],
|
||||
serialized_options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto3',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=546,
|
||||
serialized_end=817,
|
||||
)
|
||||
|
||||
|
||||
_CAMID = _descriptor.Descriptor(
|
||||
name='CamID',
|
||||
full_name='minieye.CamID',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='cam_direct', full_name='minieye.CamID.cam_direct', index=0,
|
||||
number=1, type=5, cpp_type=1, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='fov', full_name='minieye.CamID.fov', index=1,
|
||||
number=2, type=14, cpp_type=8, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='id', full_name='minieye.CamID.id', index=2,
|
||||
number=3, type=5, cpp_type=1, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
],
|
||||
serialized_options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto3',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=819,
|
||||
serialized_end=888,
|
||||
)
|
||||
|
||||
|
||||
_TRANSMATRIX = _descriptor.Descriptor(
|
||||
name='TransMatrix',
|
||||
full_name='minieye.TransMatrix',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='vcsgnd2img', full_name='minieye.TransMatrix.vcsgnd2img', index=0,
|
||||
number=1, type=2, cpp_type=6, label=3,
|
||||
has_default_value=False, default_value=[],
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='img2vcsgnd', full_name='minieye.TransMatrix.img2vcsgnd', index=1,
|
||||
number=2, type=2, cpp_type=6, label=3,
|
||||
has_default_value=False, default_value=[],
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='local2img', full_name='minieye.TransMatrix.local2img', index=2,
|
||||
number=3, type=2, cpp_type=6, label=3,
|
||||
has_default_value=False, default_value=[],
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='img2local', full_name='minieye.TransMatrix.img2local', index=3,
|
||||
number=4, type=2, cpp_type=6, label=3,
|
||||
has_default_value=False, default_value=[],
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
],
|
||||
serialized_options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto3',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=890,
|
||||
serialized_end=981,
|
||||
)
|
||||
|
||||
|
||||
_CAMERAPARAM = _descriptor.Descriptor(
|
||||
name='CameraParam',
|
||||
full_name='minieye.CameraParam',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='focal_u', full_name='minieye.CameraParam.focal_u', index=0,
|
||||
number=1, type=2, cpp_type=6, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='focal_v', full_name='minieye.CameraParam.focal_v', index=1,
|
||||
number=2, type=2, cpp_type=6, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='cu', full_name='minieye.CameraParam.cu', index=2,
|
||||
number=3, type=2, cpp_type=6, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='cv', full_name='minieye.CameraParam.cv', index=3,
|
||||
number=4, type=2, cpp_type=6, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='pos', full_name='minieye.CameraParam.pos', index=4,
|
||||
number=5, type=2, cpp_type=6, label=3,
|
||||
has_default_value=False, default_value=[],
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='pitch', full_name='minieye.CameraParam.pitch', index=5,
|
||||
number=6, type=2, cpp_type=6, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='yaw', full_name='minieye.CameraParam.yaw', index=6,
|
||||
number=7, type=2, cpp_type=6, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='roll', full_name='minieye.CameraParam.roll', index=7,
|
||||
number=8, type=2, cpp_type=6, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='fov', full_name='minieye.CameraParam.fov', index=8,
|
||||
number=9, type=2, cpp_type=6, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='image_format', full_name='minieye.CameraParam.image_format', index=9,
|
||||
number=10, type=5, cpp_type=1, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='isp_ver', full_name='minieye.CameraParam.isp_ver', index=10,
|
||||
number=11, type=9, cpp_type=9, label=1,
|
||||
has_default_value=False, default_value=b"".decode('utf-8'),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='install_direction', full_name='minieye.CameraParam.install_direction', index=11,
|
||||
number=12, type=5, cpp_type=1, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='trans_mtx', full_name='minieye.CameraParam.trans_mtx', index=12,
|
||||
number=13, type=11, cpp_type=10, label=1,
|
||||
has_default_value=False, default_value=None,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='prj_model', full_name='minieye.CameraParam.prj_model', index=13,
|
||||
number=14, type=14, cpp_type=8, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='image_width', full_name='minieye.CameraParam.image_width', index=14,
|
||||
number=15, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='image_height', full_name='minieye.CameraParam.image_height', index=15,
|
||||
number=16, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='distort_coeffs', full_name='minieye.CameraParam.distort_coeffs', index=16,
|
||||
number=17, type=1, cpp_type=5, label=3,
|
||||
has_default_value=False, default_value=[],
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='camera_id', full_name='minieye.CameraParam.camera_id', index=17,
|
||||
number=18, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='is_valid', full_name='minieye.CameraParam.is_valid', index=18,
|
||||
number=19, type=8, cpp_type=7, label=1,
|
||||
has_default_value=False, default_value=False,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='calib_method', full_name='minieye.CameraParam.calib_method', index=19,
|
||||
number=20, type=5, cpp_type=1, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='calib_timestamp', full_name='minieye.CameraParam.calib_timestamp', index=20,
|
||||
number=21, type=4, cpp_type=4, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='rvec', full_name='minieye.CameraParam.rvec', index=21,
|
||||
number=22, type=2, cpp_type=6, label=3,
|
||||
has_default_value=False, default_value=[],
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='tvec', full_name='minieye.CameraParam.tvec', index=22,
|
||||
number=23, type=2, cpp_type=6, label=3,
|
||||
has_default_value=False, default_value=[],
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='s', full_name='minieye.CameraParam.s', index=23,
|
||||
number=24, type=2, cpp_type=6, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='vehicle_width', full_name='minieye.CameraParam.vehicle_width', index=24,
|
||||
number=25, type=2, cpp_type=6, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='wheel_base', full_name='minieye.CameraParam.wheel_base', index=25,
|
||||
number=26, type=2, cpp_type=6, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='front_overhang', full_name='minieye.CameraParam.front_overhang', index=26,
|
||||
number=27, type=2, cpp_type=6, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='affine_params', full_name='minieye.CameraParam.affine_params', index=27,
|
||||
number=28, type=2, cpp_type=6, label=3,
|
||||
has_default_value=False, default_value=[],
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='poly_coeffs', full_name='minieye.CameraParam.poly_coeffs', index=28,
|
||||
number=29, type=2, cpp_type=6, label=3,
|
||||
has_default_value=False, default_value=[],
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='inv_poly_coeffs', full_name='minieye.CameraParam.inv_poly_coeffs', index=29,
|
||||
number=30, type=2, cpp_type=6, label=3,
|
||||
has_default_value=False, default_value=[],
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='sn', full_name='minieye.CameraParam.sn', index=30,
|
||||
number=31, type=9, cpp_type=9, label=1,
|
||||
has_default_value=False, default_value=b"".decode('utf-8'),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
],
|
||||
serialized_options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto3',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=984,
|
||||
serialized_end=1613,
|
||||
)
|
||||
|
||||
|
||||
_CAMERAEMBEDDEDINFO = _descriptor.Descriptor(
|
||||
name='CameraEmbeddedInfo',
|
||||
full_name='minieye.CameraEmbeddedInfo',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='frame_count', full_name='minieye.CameraEmbeddedInfo.frame_count', index=0,
|
||||
number=1, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='expo_num', full_name='minieye.CameraEmbeddedInfo.expo_num', index=1,
|
||||
number=2, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='expo_ratio', full_name='minieye.CameraEmbeddedInfo.expo_ratio', index=2,
|
||||
number=3, type=13, cpp_type=3, label=3,
|
||||
has_default_value=False, default_value=[],
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='shutter_time', full_name='minieye.CameraEmbeddedInfo.shutter_time', index=3,
|
||||
number=4, type=13, cpp_type=3, label=3,
|
||||
has_default_value=False, default_value=[],
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='sensor_a_gain', full_name='minieye.CameraEmbeddedInfo.sensor_a_gain', index=4,
|
||||
number=5, type=2, cpp_type=6, label=3,
|
||||
has_default_value=False, default_value=[],
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='sensor_d_gain', full_name='minieye.CameraEmbeddedInfo.sensor_d_gain', index=5,
|
||||
number=6, type=2, cpp_type=6, label=3,
|
||||
has_default_value=False, default_value=[],
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='exp_start_camera_time_stamp', full_name='minieye.CameraEmbeddedInfo.exp_start_camera_time_stamp', index=6,
|
||||
number=7, type=11, cpp_type=10, label=1,
|
||||
has_default_value=False, default_value=None,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='exp_end_camera_time_stamp', full_name='minieye.CameraEmbeddedInfo.exp_end_camera_time_stamp', index=7,
|
||||
number=8, type=11, cpp_type=10, label=1,
|
||||
has_default_value=False, default_value=None,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='is_valid_bitmap', full_name='minieye.CameraEmbeddedInfo.is_valid_bitmap', index=8,
|
||||
number=64, type=4, cpp_type=4, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
],
|
||||
serialized_options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto3',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=1616,
|
||||
serialized_end=1916,
|
||||
)
|
||||
|
||||
_CAMERAFRAME.fields_by_name['extended_info'].message_type = _CAMERAFRAMEEXTENDEDINFO
|
||||
_CAMID.fields_by_name['fov'].enum_type = _CAMFOV
|
||||
_CAMERAPARAM.fields_by_name['trans_mtx'].message_type = _TRANSMATRIX
|
||||
_CAMERAPARAM.fields_by_name['prj_model'].enum_type = _PROJECTIONMODEL
|
||||
_CAMERAEMBEDDEDINFO.fields_by_name['exp_start_camera_time_stamp'].message_type = google_dot_protobuf_dot_timestamp__pb2._TIMESTAMP
|
||||
_CAMERAEMBEDDEDINFO.fields_by_name['exp_end_camera_time_stamp'].message_type = google_dot_protobuf_dot_timestamp__pb2._TIMESTAMP
|
||||
DESCRIPTOR.message_types_by_name['CameraFrameExtendedInfo'] = _CAMERAFRAMEEXTENDEDINFO
|
||||
DESCRIPTOR.message_types_by_name['CameraFrame'] = _CAMERAFRAME
|
||||
DESCRIPTOR.message_types_by_name['CamID'] = _CAMID
|
||||
DESCRIPTOR.message_types_by_name['TransMatrix'] = _TRANSMATRIX
|
||||
DESCRIPTOR.message_types_by_name['CameraParam'] = _CAMERAPARAM
|
||||
DESCRIPTOR.message_types_by_name['CameraEmbeddedInfo'] = _CAMERAEMBEDDEDINFO
|
||||
DESCRIPTOR.enum_types_by_name['ImageFormat'] = _IMAGEFORMAT
|
||||
DESCRIPTOR.enum_types_by_name['CamDirect'] = _CAMDIRECT
|
||||
DESCRIPTOR.enum_types_by_name['CamFov'] = _CAMFOV
|
||||
DESCRIPTOR.enum_types_by_name['ProjectionModel'] = _PROJECTIONMODEL
|
||||
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
|
||||
|
||||
CameraFrameExtendedInfo = _reflection.GeneratedProtocolMessageType('CameraFrameExtendedInfo', (_message.Message,), {
|
||||
'DESCRIPTOR' : _CAMERAFRAMEEXTENDEDINFO,
|
||||
'__module__' : 'camera_pb2'
|
||||
# @@protoc_insertion_point(class_scope:minieye.CameraFrameExtendedInfo)
|
||||
})
|
||||
_sym_db.RegisterMessage(CameraFrameExtendedInfo)
|
||||
|
||||
CameraFrame = _reflection.GeneratedProtocolMessageType('CameraFrame', (_message.Message,), {
|
||||
'DESCRIPTOR' : _CAMERAFRAME,
|
||||
'__module__' : 'camera_pb2'
|
||||
# @@protoc_insertion_point(class_scope:minieye.CameraFrame)
|
||||
})
|
||||
_sym_db.RegisterMessage(CameraFrame)
|
||||
|
||||
CamID = _reflection.GeneratedProtocolMessageType('CamID', (_message.Message,), {
|
||||
'DESCRIPTOR' : _CAMID,
|
||||
'__module__' : 'camera_pb2'
|
||||
# @@protoc_insertion_point(class_scope:minieye.CamID)
|
||||
})
|
||||
_sym_db.RegisterMessage(CamID)
|
||||
|
||||
TransMatrix = _reflection.GeneratedProtocolMessageType('TransMatrix', (_message.Message,), {
|
||||
'DESCRIPTOR' : _TRANSMATRIX,
|
||||
'__module__' : 'camera_pb2'
|
||||
# @@protoc_insertion_point(class_scope:minieye.TransMatrix)
|
||||
})
|
||||
_sym_db.RegisterMessage(TransMatrix)
|
||||
|
||||
CameraParam = _reflection.GeneratedProtocolMessageType('CameraParam', (_message.Message,), {
|
||||
'DESCRIPTOR' : _CAMERAPARAM,
|
||||
'__module__' : 'camera_pb2'
|
||||
# @@protoc_insertion_point(class_scope:minieye.CameraParam)
|
||||
})
|
||||
_sym_db.RegisterMessage(CameraParam)
|
||||
|
||||
CameraEmbeddedInfo = _reflection.GeneratedProtocolMessageType('CameraEmbeddedInfo', (_message.Message,), {
|
||||
'DESCRIPTOR' : _CAMERAEMBEDDEDINFO,
|
||||
'__module__' : 'camera_pb2'
|
||||
# @@protoc_insertion_point(class_scope:minieye.CameraEmbeddedInfo)
|
||||
})
|
||||
_sym_db.RegisterMessage(CameraEmbeddedInfo)
|
||||
|
||||
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
680
tools/convert_merge_tracking_bundle/pyproto/command_signal_pb2.py
Executable file
680
tools/convert_merge_tracking_bundle/pyproto/command_signal_pb2.py
Executable file
@@ -0,0 +1,680 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: command_signal.proto
|
||||
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
from google.protobuf import reflection as _reflection
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor.FileDescriptor(
|
||||
name='command_signal.proto',
|
||||
package='minieye',
|
||||
syntax='proto3',
|
||||
serialized_options=None,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
serialized_pb=b'\n\x14\x63ommand_signal.proto\x12\x07minieye\x1a\x19google/protobuf/any.proto\"\xce\x04\n\rCommandSignal\x12\x31\n\x06signal\x18\x01 \x01(\x0e\x32!.minieye.CommandSignal.SignalType\x12%\n\x07\x64\x65tails\x18\x02 \x01(\x0b\x32\x14.google.protobuf.Any\"\xe2\x03\n\nSignalType\x12\x0c\n\x08kUnknown\x10\x00\x12\x13\n\x0fkStartAutocalib\x10\x01\x12\x1a\n\x16kStartOfflineCalibrate\x10\x02\x12\x16\n\x12kModLdwSensitivity\x10\x03\x12\x16\n\x12kModFcwSensitivity\x10\x04\x12\x16\n\x12kModTsrSensitivity\x10\x05\x12\x16\n\x12kModPCWSensitivity\x10\x06\x12\x0e\n\nkFCWSwitch\x10\x07\x12\x0f\n\x0bkFVSASwitch\x10\x08\x12\x0e\n\nkHMWSwitch\x10\t\x12\x0e\n\nkIHCSwitch\x10\n\x12\x0e\n\nkLDWSwitch\x10\x0b\x12\x18\n\x14kSroverspdwarnSwitch\x10\x0c\x12\x0e\n\nkTSRSwitch\x10\r\x12\x0e\n\nkPCWSwitch\x10\x0e\x12\x0e\n\nkLKASwitch\x10\x0f\x12\x0e\n\nkESCSwitch\x10\x10\x12\x12\n\x0ekStopAutocalib\x10\x11\x12\x15\n\x11kStopOfflineCalib\x10\x12\x12\x0e\n\nkSAGSwitch\x10\x13\x12\x0e\n\nkACCSwitch\x10\x14\x12\x11\n\rkTJAICASwitch\x10\x15\x12\x0e\n\nkHMASwitch\x10\x16\x12\x1c\n\x18kObjectAlgDiagnoseSwitch\x10\x17\"\xf1\x01\n\x15OfflineCalibSigDetail\x12\x11\n\tcamera_id\x18\x01 \x01(\x05\x12\x13\n\x0bmarker_type\x18\x02 \x01(\x05\x12\x14\n\x0c\x63\x61mera_pitch\x18\x03 \x01(\x01\x12\x12\n\ncamera_yaw\x18\x04 \x01(\x01\x12\x13\n\x0b\x63\x61mera_roll\x18\x05 \x01(\x01\x12\x15\n\rcamera_height\x18\x06 \x01(\x01\x12\x1b\n\x13left_dist_to_camera\x18\x07 \x01(\x01\x12\x1c\n\x14right_dist_to_camera\x18\x08 \x01(\x01\x12\x1f\n\x17\x66ront_wheel_camera_dist\x18\t \x01(\x01\"\xac\x01\n\x0fOfflineCalibRsp\x12\x11\n\tcamera_id\x18\x01 \x01(\x05\x12\r\n\x05state\x18\x02 \x01(\x05\x12\x10\n\x08\x65rr_code\x18\x03 \x01(\x05\x12\x14\n\x0c\x63\x61mera_pitch\x18\x04 \x01(\x01\x12\x12\n\ncamera_yaw\x18\x05 \x01(\x01\x12\x13\n\x0b\x63\x61mera_roll\x18\x06 \x01(\x01\x12\x15\n\rcamera_height\x18\x07 \x01(\x01\x12\x0f\n\x07process\x18\x08 \x01(\x01\"\x9a\x01\n\x12\x41utoCalibSigDetail\x12\x11\n\tcamera_id\x18\x01 \x01(\x05\x12\x15\n\rcamera_height\x18\x02 \x01(\x01\x12\x1b\n\x13left_dist_to_camera\x18\x03 \x01(\x01\x12\x1c\n\x14right_dist_to_camera\x18\x04 \x01(\x01\x12\x1f\n\x17\x66ront_wheel_camera_dist\x18\x05 \x01(\x01\"\xa9\x01\n\x0c\x41utoCalibRsp\x12\x11\n\tcamera_id\x18\x01 \x01(\x05\x12\r\n\x05state\x18\x02 \x01(\x05\x12\x10\n\x08\x65rr_code\x18\x03 \x01(\x05\x12\x14\n\x0c\x63\x61mera_pitch\x18\x04 \x01(\x01\x12\x12\n\ncamera_yaw\x18\x05 \x01(\x01\x12\x13\n\x0b\x63\x61mera_roll\x18\x06 \x01(\x01\x12\x15\n\rcamera_height\x18\x07 \x01(\x01\x12\x0f\n\x07process\x18\x08 \x01(\x01\"-\n\x14SensitivitySigDetail\x12\x15\n\rwarning_level\x18\x01 \x01(\x05\"\'\n\x0fSwitchSigDetail\x12\x14\n\x0cswitch_value\x18\x01 \x01(\x05\")\n\x17ObjectAlgDiagnoseDetail\x12\x0e\n\x06seq_no\x18\x01 \x01(\x04\x62\x06proto3'
|
||||
,
|
||||
dependencies=[google_dot_protobuf_dot_any__pb2.DESCRIPTOR,])
|
||||
|
||||
|
||||
|
||||
_COMMANDSIGNAL_SIGNALTYPE = _descriptor.EnumDescriptor(
|
||||
name='SignalType',
|
||||
full_name='minieye.CommandSignal.SignalType',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
values=[
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kUnknown', index=0, number=0,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kStartAutocalib', index=1, number=1,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kStartOfflineCalibrate', index=2, number=2,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kModLdwSensitivity', index=3, number=3,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kModFcwSensitivity', index=4, number=4,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kModTsrSensitivity', index=5, number=5,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kModPCWSensitivity', index=6, number=6,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kFCWSwitch', index=7, number=7,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kFVSASwitch', index=8, number=8,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kHMWSwitch', index=9, number=9,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kIHCSwitch', index=10, number=10,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kLDWSwitch', index=11, number=11,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSroverspdwarnSwitch', index=12, number=12,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kTSRSwitch', index=13, number=13,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kPCWSwitch', index=14, number=14,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kLKASwitch', index=15, number=15,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kESCSwitch', index=16, number=16,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kStopAutocalib', index=17, number=17,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kStopOfflineCalib', index=18, number=18,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSAGSwitch', index=19, number=19,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kACCSwitch', index=20, number=20,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kTJAICASwitch', index=21, number=21,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kHMASwitch', index=22, number=22,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kObjectAlgDiagnoseSwitch', index=23, number=23,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
containing_type=None,
|
||||
serialized_options=None,
|
||||
serialized_start=169,
|
||||
serialized_end=651,
|
||||
)
|
||||
_sym_db.RegisterEnumDescriptor(_COMMANDSIGNAL_SIGNALTYPE)
|
||||
|
||||
|
||||
_COMMANDSIGNAL = _descriptor.Descriptor(
|
||||
name='CommandSignal',
|
||||
full_name='minieye.CommandSignal',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='signal', full_name='minieye.CommandSignal.signal', index=0,
|
||||
number=1, type=14, cpp_type=8, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='details', full_name='minieye.CommandSignal.details', index=1,
|
||||
number=2, type=11, cpp_type=10, label=1,
|
||||
has_default_value=False, default_value=None,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
_COMMANDSIGNAL_SIGNALTYPE,
|
||||
],
|
||||
serialized_options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto3',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=61,
|
||||
serialized_end=651,
|
||||
)
|
||||
|
||||
|
||||
_OFFLINECALIBSIGDETAIL = _descriptor.Descriptor(
|
||||
name='OfflineCalibSigDetail',
|
||||
full_name='minieye.OfflineCalibSigDetail',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='camera_id', full_name='minieye.OfflineCalibSigDetail.camera_id', index=0,
|
||||
number=1, type=5, cpp_type=1, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='marker_type', full_name='minieye.OfflineCalibSigDetail.marker_type', index=1,
|
||||
number=2, type=5, cpp_type=1, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='camera_pitch', full_name='minieye.OfflineCalibSigDetail.camera_pitch', index=2,
|
||||
number=3, type=1, cpp_type=5, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='camera_yaw', full_name='minieye.OfflineCalibSigDetail.camera_yaw', index=3,
|
||||
number=4, type=1, cpp_type=5, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='camera_roll', full_name='minieye.OfflineCalibSigDetail.camera_roll', index=4,
|
||||
number=5, type=1, cpp_type=5, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='camera_height', full_name='minieye.OfflineCalibSigDetail.camera_height', index=5,
|
||||
number=6, type=1, cpp_type=5, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='left_dist_to_camera', full_name='minieye.OfflineCalibSigDetail.left_dist_to_camera', index=6,
|
||||
number=7, type=1, cpp_type=5, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='right_dist_to_camera', full_name='minieye.OfflineCalibSigDetail.right_dist_to_camera', index=7,
|
||||
number=8, type=1, cpp_type=5, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='front_wheel_camera_dist', full_name='minieye.OfflineCalibSigDetail.front_wheel_camera_dist', index=8,
|
||||
number=9, type=1, cpp_type=5, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
],
|
||||
serialized_options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto3',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=654,
|
||||
serialized_end=895,
|
||||
)
|
||||
|
||||
|
||||
_OFFLINECALIBRSP = _descriptor.Descriptor(
|
||||
name='OfflineCalibRsp',
|
||||
full_name='minieye.OfflineCalibRsp',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='camera_id', full_name='minieye.OfflineCalibRsp.camera_id', index=0,
|
||||
number=1, type=5, cpp_type=1, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='state', full_name='minieye.OfflineCalibRsp.state', index=1,
|
||||
number=2, type=5, cpp_type=1, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='err_code', full_name='minieye.OfflineCalibRsp.err_code', index=2,
|
||||
number=3, type=5, cpp_type=1, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='camera_pitch', full_name='minieye.OfflineCalibRsp.camera_pitch', index=3,
|
||||
number=4, type=1, cpp_type=5, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='camera_yaw', full_name='minieye.OfflineCalibRsp.camera_yaw', index=4,
|
||||
number=5, type=1, cpp_type=5, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='camera_roll', full_name='minieye.OfflineCalibRsp.camera_roll', index=5,
|
||||
number=6, type=1, cpp_type=5, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='camera_height', full_name='minieye.OfflineCalibRsp.camera_height', index=6,
|
||||
number=7, type=1, cpp_type=5, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='process', full_name='minieye.OfflineCalibRsp.process', index=7,
|
||||
number=8, type=1, cpp_type=5, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
],
|
||||
serialized_options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto3',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=898,
|
||||
serialized_end=1070,
|
||||
)
|
||||
|
||||
|
||||
_AUTOCALIBSIGDETAIL = _descriptor.Descriptor(
|
||||
name='AutoCalibSigDetail',
|
||||
full_name='minieye.AutoCalibSigDetail',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='camera_id', full_name='minieye.AutoCalibSigDetail.camera_id', index=0,
|
||||
number=1, type=5, cpp_type=1, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='camera_height', full_name='minieye.AutoCalibSigDetail.camera_height', index=1,
|
||||
number=2, type=1, cpp_type=5, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='left_dist_to_camera', full_name='minieye.AutoCalibSigDetail.left_dist_to_camera', index=2,
|
||||
number=3, type=1, cpp_type=5, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='right_dist_to_camera', full_name='minieye.AutoCalibSigDetail.right_dist_to_camera', index=3,
|
||||
number=4, type=1, cpp_type=5, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='front_wheel_camera_dist', full_name='minieye.AutoCalibSigDetail.front_wheel_camera_dist', index=4,
|
||||
number=5, type=1, cpp_type=5, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
],
|
||||
serialized_options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto3',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=1073,
|
||||
serialized_end=1227,
|
||||
)
|
||||
|
||||
|
||||
_AUTOCALIBRSP = _descriptor.Descriptor(
|
||||
name='AutoCalibRsp',
|
||||
full_name='minieye.AutoCalibRsp',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='camera_id', full_name='minieye.AutoCalibRsp.camera_id', index=0,
|
||||
number=1, type=5, cpp_type=1, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='state', full_name='minieye.AutoCalibRsp.state', index=1,
|
||||
number=2, type=5, cpp_type=1, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='err_code', full_name='minieye.AutoCalibRsp.err_code', index=2,
|
||||
number=3, type=5, cpp_type=1, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='camera_pitch', full_name='minieye.AutoCalibRsp.camera_pitch', index=3,
|
||||
number=4, type=1, cpp_type=5, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='camera_yaw', full_name='minieye.AutoCalibRsp.camera_yaw', index=4,
|
||||
number=5, type=1, cpp_type=5, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='camera_roll', full_name='minieye.AutoCalibRsp.camera_roll', index=5,
|
||||
number=6, type=1, cpp_type=5, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='camera_height', full_name='minieye.AutoCalibRsp.camera_height', index=6,
|
||||
number=7, type=1, cpp_type=5, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='process', full_name='minieye.AutoCalibRsp.process', index=7,
|
||||
number=8, type=1, cpp_type=5, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
],
|
||||
serialized_options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto3',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=1230,
|
||||
serialized_end=1399,
|
||||
)
|
||||
|
||||
|
||||
_SENSITIVITYSIGDETAIL = _descriptor.Descriptor(
|
||||
name='SensitivitySigDetail',
|
||||
full_name='minieye.SensitivitySigDetail',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='warning_level', full_name='minieye.SensitivitySigDetail.warning_level', index=0,
|
||||
number=1, type=5, cpp_type=1, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
],
|
||||
serialized_options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto3',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=1401,
|
||||
serialized_end=1446,
|
||||
)
|
||||
|
||||
|
||||
_SWITCHSIGDETAIL = _descriptor.Descriptor(
|
||||
name='SwitchSigDetail',
|
||||
full_name='minieye.SwitchSigDetail',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='switch_value', full_name='minieye.SwitchSigDetail.switch_value', index=0,
|
||||
number=1, type=5, cpp_type=1, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
],
|
||||
serialized_options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto3',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=1448,
|
||||
serialized_end=1487,
|
||||
)
|
||||
|
||||
|
||||
_OBJECTALGDIAGNOSEDETAIL = _descriptor.Descriptor(
|
||||
name='ObjectAlgDiagnoseDetail',
|
||||
full_name='minieye.ObjectAlgDiagnoseDetail',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='seq_no', full_name='minieye.ObjectAlgDiagnoseDetail.seq_no', index=0,
|
||||
number=1, type=4, cpp_type=4, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
],
|
||||
serialized_options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto3',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=1489,
|
||||
serialized_end=1530,
|
||||
)
|
||||
|
||||
_COMMANDSIGNAL.fields_by_name['signal'].enum_type = _COMMANDSIGNAL_SIGNALTYPE
|
||||
_COMMANDSIGNAL.fields_by_name['details'].message_type = google_dot_protobuf_dot_any__pb2._ANY
|
||||
_COMMANDSIGNAL_SIGNALTYPE.containing_type = _COMMANDSIGNAL
|
||||
DESCRIPTOR.message_types_by_name['CommandSignal'] = _COMMANDSIGNAL
|
||||
DESCRIPTOR.message_types_by_name['OfflineCalibSigDetail'] = _OFFLINECALIBSIGDETAIL
|
||||
DESCRIPTOR.message_types_by_name['OfflineCalibRsp'] = _OFFLINECALIBRSP
|
||||
DESCRIPTOR.message_types_by_name['AutoCalibSigDetail'] = _AUTOCALIBSIGDETAIL
|
||||
DESCRIPTOR.message_types_by_name['AutoCalibRsp'] = _AUTOCALIBRSP
|
||||
DESCRIPTOR.message_types_by_name['SensitivitySigDetail'] = _SENSITIVITYSIGDETAIL
|
||||
DESCRIPTOR.message_types_by_name['SwitchSigDetail'] = _SWITCHSIGDETAIL
|
||||
DESCRIPTOR.message_types_by_name['ObjectAlgDiagnoseDetail'] = _OBJECTALGDIAGNOSEDETAIL
|
||||
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
|
||||
|
||||
CommandSignal = _reflection.GeneratedProtocolMessageType('CommandSignal', (_message.Message,), {
|
||||
'DESCRIPTOR' : _COMMANDSIGNAL,
|
||||
'__module__' : 'command_signal_pb2'
|
||||
# @@protoc_insertion_point(class_scope:minieye.CommandSignal)
|
||||
})
|
||||
_sym_db.RegisterMessage(CommandSignal)
|
||||
|
||||
OfflineCalibSigDetail = _reflection.GeneratedProtocolMessageType('OfflineCalibSigDetail', (_message.Message,), {
|
||||
'DESCRIPTOR' : _OFFLINECALIBSIGDETAIL,
|
||||
'__module__' : 'command_signal_pb2'
|
||||
# @@protoc_insertion_point(class_scope:minieye.OfflineCalibSigDetail)
|
||||
})
|
||||
_sym_db.RegisterMessage(OfflineCalibSigDetail)
|
||||
|
||||
OfflineCalibRsp = _reflection.GeneratedProtocolMessageType('OfflineCalibRsp', (_message.Message,), {
|
||||
'DESCRIPTOR' : _OFFLINECALIBRSP,
|
||||
'__module__' : 'command_signal_pb2'
|
||||
# @@protoc_insertion_point(class_scope:minieye.OfflineCalibRsp)
|
||||
})
|
||||
_sym_db.RegisterMessage(OfflineCalibRsp)
|
||||
|
||||
AutoCalibSigDetail = _reflection.GeneratedProtocolMessageType('AutoCalibSigDetail', (_message.Message,), {
|
||||
'DESCRIPTOR' : _AUTOCALIBSIGDETAIL,
|
||||
'__module__' : 'command_signal_pb2'
|
||||
# @@protoc_insertion_point(class_scope:minieye.AutoCalibSigDetail)
|
||||
})
|
||||
_sym_db.RegisterMessage(AutoCalibSigDetail)
|
||||
|
||||
AutoCalibRsp = _reflection.GeneratedProtocolMessageType('AutoCalibRsp', (_message.Message,), {
|
||||
'DESCRIPTOR' : _AUTOCALIBRSP,
|
||||
'__module__' : 'command_signal_pb2'
|
||||
# @@protoc_insertion_point(class_scope:minieye.AutoCalibRsp)
|
||||
})
|
||||
_sym_db.RegisterMessage(AutoCalibRsp)
|
||||
|
||||
SensitivitySigDetail = _reflection.GeneratedProtocolMessageType('SensitivitySigDetail', (_message.Message,), {
|
||||
'DESCRIPTOR' : _SENSITIVITYSIGDETAIL,
|
||||
'__module__' : 'command_signal_pb2'
|
||||
# @@protoc_insertion_point(class_scope:minieye.SensitivitySigDetail)
|
||||
})
|
||||
_sym_db.RegisterMessage(SensitivitySigDetail)
|
||||
|
||||
SwitchSigDetail = _reflection.GeneratedProtocolMessageType('SwitchSigDetail', (_message.Message,), {
|
||||
'DESCRIPTOR' : _SWITCHSIGDETAIL,
|
||||
'__module__' : 'command_signal_pb2'
|
||||
# @@protoc_insertion_point(class_scope:minieye.SwitchSigDetail)
|
||||
})
|
||||
_sym_db.RegisterMessage(SwitchSigDetail)
|
||||
|
||||
ObjectAlgDiagnoseDetail = _reflection.GeneratedProtocolMessageType('ObjectAlgDiagnoseDetail', (_message.Message,), {
|
||||
'DESCRIPTOR' : _OBJECTALGDIAGNOSEDETAIL,
|
||||
'__module__' : 'command_signal_pb2'
|
||||
# @@protoc_insertion_point(class_scope:minieye.ObjectAlgDiagnoseDetail)
|
||||
})
|
||||
_sym_db.RegisterMessage(ObjectAlgDiagnoseDetail)
|
||||
|
||||
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
96
tools/convert_merge_tracking_bundle/pyproto/data_source_pb2.py
Executable file
96
tools/convert_merge_tracking_bundle/pyproto/data_source_pb2.py
Executable file
@@ -0,0 +1,96 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: data_source.proto
|
||||
|
||||
from google.protobuf.internal import enum_type_wrapper
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
from google.protobuf import reflection as _reflection
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor.FileDescriptor(
|
||||
name='data_source.proto',
|
||||
package='minieye',
|
||||
syntax='proto3',
|
||||
serialized_options=None,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
serialized_pb=b'\n\x11\x64\x61ta_source.proto\x12\x07minieye*w\n\nDataSource\x12\x0c\n\x08kMinieye\x10\x00\x12\n\n\x06kEyeQ3\x10\x01\x12\n\n\x06kEyeQ4\x10\x02\x12\x07\n\x03kJ2\x10\x03\x12\n\n\x06kLidar\x10\x04\x12\x0b\n\x07kCamera\x10\x05\x12\x10\n\x0ckCameraLidar\x10\x06\x12\x0f\n\x0bkUltraRadar\x10\x07\x62\x06proto3'
|
||||
)
|
||||
|
||||
_DATASOURCE = _descriptor.EnumDescriptor(
|
||||
name='DataSource',
|
||||
full_name='minieye.DataSource',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
values=[
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kMinieye', index=0, number=0,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kEyeQ3', index=1, number=1,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kEyeQ4', index=2, number=2,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kJ2', index=3, number=3,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kLidar', index=4, number=4,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kCamera', index=5, number=5,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kCameraLidar', index=6, number=6,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kUltraRadar', index=7, number=7,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
containing_type=None,
|
||||
serialized_options=None,
|
||||
serialized_start=30,
|
||||
serialized_end=149,
|
||||
)
|
||||
_sym_db.RegisterEnumDescriptor(_DATASOURCE)
|
||||
|
||||
DataSource = enum_type_wrapper.EnumTypeWrapper(_DATASOURCE)
|
||||
kMinieye = 0
|
||||
kEyeQ3 = 1
|
||||
kEyeQ4 = 2
|
||||
kJ2 = 3
|
||||
kLidar = 4
|
||||
kCamera = 5
|
||||
kCameraLidar = 6
|
||||
kUltraRadar = 7
|
||||
|
||||
|
||||
DESCRIPTOR.enum_types_by_name['DataSource'] = _DATASOURCE
|
||||
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
|
||||
|
||||
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
1638
tools/convert_merge_tracking_bundle/pyproto/geometry_pb2.py
Executable file
1638
tools/convert_merge_tracking_bundle/pyproto/geometry_pb2.py
Executable file
File diff suppressed because one or more lines are too long
3008
tools/convert_merge_tracking_bundle/pyproto/object_pb2.py
Executable file
3008
tools/convert_merge_tracking_bundle/pyproto/object_pb2.py
Executable file
File diff suppressed because one or more lines are too long
3326
tools/convert_merge_tracking_bundle/pyproto/object_pb2_new.py
Executable file
3326
tools/convert_merge_tracking_bundle/pyproto/object_pb2_new.py
Executable file
File diff suppressed because one or more lines are too long
1102
tools/convert_merge_tracking_bundle/pyproto/object_warning_pb2.py
Executable file
1102
tools/convert_merge_tracking_bundle/pyproto/object_warning_pb2.py
Executable file
File diff suppressed because it is too large
Load Diff
848
tools/convert_merge_tracking_bundle/pyproto/scene_pb2.py
Executable file
848
tools/convert_merge_tracking_bundle/pyproto/scene_pb2.py
Executable file
@@ -0,0 +1,848 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: scene.proto
|
||||
|
||||
from google.protobuf.internal import enum_type_wrapper
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
from google.protobuf import reflection as _reflection
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
import camera_pb2 as camera__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor.FileDescriptor(
|
||||
name='scene.proto',
|
||||
package='perception',
|
||||
syntax='proto3',
|
||||
serialized_options=None,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
serialized_pb=b'\n\x0bscene.proto\x12\nperception\x1a\x0c\x63\x61mera.proto\"\xac\x03\n\x05Scene\x12\x31\n\x0bshelter_cls\x18\x01 \x01(\x0e\x32\x1c.perception.ShelterSceneType\x12+\n\x08road_cls\x18\x02 \x01(\x0e\x32\x19.perception.RoadSceneType\x12@\n\x10lighting_dir_cls\x18\x03 \x01(\x0e\x32&.perception.LightingDirectionSceneType\x12,\n\x0bterrain_cls\x18\x04 \x01(\x0e\x32\x17.perception.TerrainType\x12\x33\n\x0clighting_cls\x18\x05 \x01(\x0e\x32\x1d.perception.LightingSceneType\x12\x1e\n\x06\x63\x61m_id\x18\x06 \x01(\x0b\x32\x0e.minieye.CamID\x12/\n\nground_cls\x18\x07 \x01(\x0e\x32\x1b.perception.GroundSceneType\x12\x14\n\x0cshelter_part\x18\x08 \x01(\r\x12\x10\n\x08\x66rame_id\x18\x0f \x01(\x04\x12\x14\n\x0ctimestamp_us\x18\x10 \x01(\x04\x12\x0f\n\x07tick_us\x18\x11 \x01(\x04\"\xb3\x01\n\tSceneList\x12%\n\nscene_list\x18\x01 \x03(\x0b\x32\x11.perception.Scene\x12\x0b\n\x03lux\x18\x02 \x01(\x02\x12\x12\n\nihbc_state\x18\x03 \x01(\r\x12\x10\n\x08\x66rame_id\x18\x06 \x01(\x04\x12\x14\n\x0ctimestamp_us\x18\x07 \x01(\x04\x12\x0f\n\x07tick_us\x18\x08 \x01(\x04\x12%\n\ndebug_list\x18\t \x03(\x0b\x32\x11.perception.Scene\"c\n\x0b\x43\x61meraScene\x12 \n\x05scene\x18\x01 \x01(\x0b\x32\x11.perception.Scene\x12\x11\n\tcamera_id\x18\x02 \x01(\x04\x12\x11\n\ttimestamp\x18\x04 \x01(\x04\x12\x0c\n\x04tick\x18\x05 \x01(\x04*\x86\x01\n\x0bWeatherType\x12\x12\n\x0eWEATHER_NORMAL\x10\x00\x12\x13\n\x0fWEATHER_NORMAL2\x10\x01\x12\x11\n\rWEATHER_RAINY\x10\x02\x12\x11\n\rWEATHER_SNOWY\x10\x03\x12\x15\n\x11WEATHER_HEAVYRAIN\x10\x04\x12\x11\n\rWEATHER_OTHER\x10\x05*|\n\tSceneType\x12\x11\n\rSCENE_HIGHWAY\x10\x00\x12\x0f\n\x0bSCENE_URBAN\x10\x01\x12\x0f\n\x0bSCENE_RURAL\x10\x02\x12\x10\n\x0cSCENE_TUNNEL\x10\x03\x12\x0f\n\x0bSCENE_OTHER\x10\x05\x12\x17\n\x13SCENE_CHARGESTATION\x10\x04*8\n\x08TimeType\x12\x0c\n\x08TIME_DAY\x10\x00\x12\x0e\n\nTIME_NIGHT\x10\x01\x12\x0e\n\nTIME_OTHER\x10\x02*\x80\x01\n\tLightType\x12\x16\n\x12LIGHT_NATRUALLIGHT\x10\x00\x12\x13\n\x0fLIGHT_LAMPLIGHT\x10\x01\x12\x13\n\x0fLIGHT_HARDLIGHT\x10\x02\x12\x10\n\x0cLIGHT_LOWSUN\x10\x03\x12\x0e\n\nLIGHT_DARK\x10\x04\x12\x0f\n\x0bLIGHT_OTHER\x10\x05*|\n\x11WorkConditionType\x12\x1a\n\x16WORK_CONDITION_WEATHER\x10\x00\x12\x18\n\x14WORK_CONDITION_LIGHT\x10\x01\x12\x18\n\x14WORK_CONDITION_SCENE\x10\x02\x12\x17\n\x13WORK_CONDITION_TIME\x10\x03*\x9e\x01\n\x11ShelterScenepPart\x12\x19\n\x15kShelterScenePartNone\x10\x00\x12\x18\n\x14kShelterSceneLeftTop\x10\x01\x12\x19\n\x15kShelterSceneRightTop\x10\x02\x12\x1b\n\x17kShelterSceneLeftBottom\x10\x04\x12\x1c\n\x18kShelterSceneRightBottom\x10\x08*k\n\x10ShelterSceneType\x12\x13\n\x0fkSceneNoShelter\x10\x00\x12\x11\n\rkSceneShelter\x10\x01\x12\x18\n\x14kSceneQuarterShelter\x10\x02\x12\x15\n\x11kSceneHalfShelter\x10\x03*\xb5\x01\n\rRoadSceneType\x12\x0f\n\x0bkSceneUrban\x10\x00\x12\x11\n\rkSceneHighway\x10\x01\x12\x18\n\x14kSceneOpenAirParking\x10\x02\x12\x11\n\rkSceneParking\x10\x03\x12\x18\n\x14kSceneTunnelEntrance\x10\x04\x12\x10\n\x0ckSceneTunnel\x10\x05\x12\x14\n\x10kSceneTunnelExit\x10\x06\x12\x11\n\rkSceneViaduct\x10\x07*M\n\x1aLightingDirectionSceneType\x12\x17\n\x13kSceneFrontlighting\x10\x00\x12\x16\n\x12kSceneBacklighting\x10\x01*T\n\x0fGroundSceneType\x12\r\n\tkSceneDry\x10\x00\x12\x11\n\rkScenePonding\x10\x01\x12\x0e\n\nkSceneSnow\x10\x02\x12\x0f\n\x0bkSceneBumpy\x10\x03*\x92\x01\n\x0bTerrainType\x12\x12\n\x0ekTerrainNormal\x10\x00\x12\x11\n\rkTerrainGrass\x10\x01\x12\x12\n\x0ekTerrainDesert\x10\x02\x12\x12\n\x0ekTerrainWading\x10\x03\x12\x11\n\rkTerrainRocks\x10\x04\x12\x10\n\x0ckTerrainSnow\x10\x05\x12\x0f\n\x0bkTerrainMud\x10\x06*c\n\x11LightingSceneType\x12\r\n\tkSceneDay\x10\x00\x12\x1d\n\x19kSceneNightWithStreetLamp\x10\x01\x12 \n\x1ckSceneNightWithoutStreetLamp\x10\x02\x62\x06proto3'
|
||||
,
|
||||
dependencies=[camera__pb2.DESCRIPTOR,])
|
||||
|
||||
_WEATHERTYPE = _descriptor.EnumDescriptor(
|
||||
name='WeatherType',
|
||||
full_name='perception.WeatherType',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
values=[
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='WEATHER_NORMAL', index=0, number=0,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='WEATHER_NORMAL2', index=1, number=1,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='WEATHER_RAINY', index=2, number=2,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='WEATHER_SNOWY', index=3, number=3,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='WEATHER_HEAVYRAIN', index=4, number=4,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='WEATHER_OTHER', index=5, number=5,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
containing_type=None,
|
||||
serialized_options=None,
|
||||
serialized_start=756,
|
||||
serialized_end=890,
|
||||
)
|
||||
_sym_db.RegisterEnumDescriptor(_WEATHERTYPE)
|
||||
|
||||
WeatherType = enum_type_wrapper.EnumTypeWrapper(_WEATHERTYPE)
|
||||
_SCENETYPE = _descriptor.EnumDescriptor(
|
||||
name='SceneType',
|
||||
full_name='perception.SceneType',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
values=[
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='SCENE_HIGHWAY', index=0, number=0,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='SCENE_URBAN', index=1, number=1,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='SCENE_RURAL', index=2, number=2,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='SCENE_TUNNEL', index=3, number=3,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='SCENE_OTHER', index=4, number=5,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='SCENE_CHARGESTATION', index=5, number=4,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
containing_type=None,
|
||||
serialized_options=None,
|
||||
serialized_start=892,
|
||||
serialized_end=1016,
|
||||
)
|
||||
_sym_db.RegisterEnumDescriptor(_SCENETYPE)
|
||||
|
||||
SceneType = enum_type_wrapper.EnumTypeWrapper(_SCENETYPE)
|
||||
_TIMETYPE = _descriptor.EnumDescriptor(
|
||||
name='TimeType',
|
||||
full_name='perception.TimeType',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
values=[
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='TIME_DAY', index=0, number=0,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='TIME_NIGHT', index=1, number=1,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='TIME_OTHER', index=2, number=2,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
containing_type=None,
|
||||
serialized_options=None,
|
||||
serialized_start=1018,
|
||||
serialized_end=1074,
|
||||
)
|
||||
_sym_db.RegisterEnumDescriptor(_TIMETYPE)
|
||||
|
||||
TimeType = enum_type_wrapper.EnumTypeWrapper(_TIMETYPE)
|
||||
_LIGHTTYPE = _descriptor.EnumDescriptor(
|
||||
name='LightType',
|
||||
full_name='perception.LightType',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
values=[
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='LIGHT_NATRUALLIGHT', index=0, number=0,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='LIGHT_LAMPLIGHT', index=1, number=1,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='LIGHT_HARDLIGHT', index=2, number=2,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='LIGHT_LOWSUN', index=3, number=3,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='LIGHT_DARK', index=4, number=4,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='LIGHT_OTHER', index=5, number=5,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
containing_type=None,
|
||||
serialized_options=None,
|
||||
serialized_start=1077,
|
||||
serialized_end=1205,
|
||||
)
|
||||
_sym_db.RegisterEnumDescriptor(_LIGHTTYPE)
|
||||
|
||||
LightType = enum_type_wrapper.EnumTypeWrapper(_LIGHTTYPE)
|
||||
_WORKCONDITIONTYPE = _descriptor.EnumDescriptor(
|
||||
name='WorkConditionType',
|
||||
full_name='perception.WorkConditionType',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
values=[
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='WORK_CONDITION_WEATHER', index=0, number=0,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='WORK_CONDITION_LIGHT', index=1, number=1,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='WORK_CONDITION_SCENE', index=2, number=2,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='WORK_CONDITION_TIME', index=3, number=3,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
containing_type=None,
|
||||
serialized_options=None,
|
||||
serialized_start=1207,
|
||||
serialized_end=1331,
|
||||
)
|
||||
_sym_db.RegisterEnumDescriptor(_WORKCONDITIONTYPE)
|
||||
|
||||
WorkConditionType = enum_type_wrapper.EnumTypeWrapper(_WORKCONDITIONTYPE)
|
||||
_SHELTERSCENEPPART = _descriptor.EnumDescriptor(
|
||||
name='ShelterScenepPart',
|
||||
full_name='perception.ShelterScenepPart',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
values=[
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kShelterScenePartNone', index=0, number=0,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kShelterSceneLeftTop', index=1, number=1,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kShelterSceneRightTop', index=2, number=2,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kShelterSceneLeftBottom', index=3, number=4,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kShelterSceneRightBottom', index=4, number=8,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
containing_type=None,
|
||||
serialized_options=None,
|
||||
serialized_start=1334,
|
||||
serialized_end=1492,
|
||||
)
|
||||
_sym_db.RegisterEnumDescriptor(_SHELTERSCENEPPART)
|
||||
|
||||
ShelterScenepPart = enum_type_wrapper.EnumTypeWrapper(_SHELTERSCENEPPART)
|
||||
_SHELTERSCENETYPE = _descriptor.EnumDescriptor(
|
||||
name='ShelterSceneType',
|
||||
full_name='perception.ShelterSceneType',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
values=[
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSceneNoShelter', index=0, number=0,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSceneShelter', index=1, number=1,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSceneQuarterShelter', index=2, number=2,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSceneHalfShelter', index=3, number=3,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
containing_type=None,
|
||||
serialized_options=None,
|
||||
serialized_start=1494,
|
||||
serialized_end=1601,
|
||||
)
|
||||
_sym_db.RegisterEnumDescriptor(_SHELTERSCENETYPE)
|
||||
|
||||
ShelterSceneType = enum_type_wrapper.EnumTypeWrapper(_SHELTERSCENETYPE)
|
||||
_ROADSCENETYPE = _descriptor.EnumDescriptor(
|
||||
name='RoadSceneType',
|
||||
full_name='perception.RoadSceneType',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
values=[
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSceneUrban', index=0, number=0,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSceneHighway', index=1, number=1,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSceneOpenAirParking', index=2, number=2,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSceneParking', index=3, number=3,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSceneTunnelEntrance', index=4, number=4,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSceneTunnel', index=5, number=5,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSceneTunnelExit', index=6, number=6,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSceneViaduct', index=7, number=7,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
containing_type=None,
|
||||
serialized_options=None,
|
||||
serialized_start=1604,
|
||||
serialized_end=1785,
|
||||
)
|
||||
_sym_db.RegisterEnumDescriptor(_ROADSCENETYPE)
|
||||
|
||||
RoadSceneType = enum_type_wrapper.EnumTypeWrapper(_ROADSCENETYPE)
|
||||
_LIGHTINGDIRECTIONSCENETYPE = _descriptor.EnumDescriptor(
|
||||
name='LightingDirectionSceneType',
|
||||
full_name='perception.LightingDirectionSceneType',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
values=[
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSceneFrontlighting', index=0, number=0,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSceneBacklighting', index=1, number=1,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
containing_type=None,
|
||||
serialized_options=None,
|
||||
serialized_start=1787,
|
||||
serialized_end=1864,
|
||||
)
|
||||
_sym_db.RegisterEnumDescriptor(_LIGHTINGDIRECTIONSCENETYPE)
|
||||
|
||||
LightingDirectionSceneType = enum_type_wrapper.EnumTypeWrapper(_LIGHTINGDIRECTIONSCENETYPE)
|
||||
_GROUNDSCENETYPE = _descriptor.EnumDescriptor(
|
||||
name='GroundSceneType',
|
||||
full_name='perception.GroundSceneType',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
values=[
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSceneDry', index=0, number=0,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kScenePonding', index=1, number=1,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSceneSnow', index=2, number=2,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSceneBumpy', index=3, number=3,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
containing_type=None,
|
||||
serialized_options=None,
|
||||
serialized_start=1866,
|
||||
serialized_end=1950,
|
||||
)
|
||||
_sym_db.RegisterEnumDescriptor(_GROUNDSCENETYPE)
|
||||
|
||||
GroundSceneType = enum_type_wrapper.EnumTypeWrapper(_GROUNDSCENETYPE)
|
||||
_TERRAINTYPE = _descriptor.EnumDescriptor(
|
||||
name='TerrainType',
|
||||
full_name='perception.TerrainType',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
values=[
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kTerrainNormal', index=0, number=0,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kTerrainGrass', index=1, number=1,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kTerrainDesert', index=2, number=2,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kTerrainWading', index=3, number=3,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kTerrainRocks', index=4, number=4,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kTerrainSnow', index=5, number=5,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kTerrainMud', index=6, number=6,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
containing_type=None,
|
||||
serialized_options=None,
|
||||
serialized_start=1953,
|
||||
serialized_end=2099,
|
||||
)
|
||||
_sym_db.RegisterEnumDescriptor(_TERRAINTYPE)
|
||||
|
||||
TerrainType = enum_type_wrapper.EnumTypeWrapper(_TERRAINTYPE)
|
||||
_LIGHTINGSCENETYPE = _descriptor.EnumDescriptor(
|
||||
name='LightingSceneType',
|
||||
full_name='perception.LightingSceneType',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
values=[
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSceneDay', index=0, number=0,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSceneNightWithStreetLamp', index=1, number=1,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSceneNightWithoutStreetLamp', index=2, number=2,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
containing_type=None,
|
||||
serialized_options=None,
|
||||
serialized_start=2101,
|
||||
serialized_end=2200,
|
||||
)
|
||||
_sym_db.RegisterEnumDescriptor(_LIGHTINGSCENETYPE)
|
||||
|
||||
LightingSceneType = enum_type_wrapper.EnumTypeWrapper(_LIGHTINGSCENETYPE)
|
||||
WEATHER_NORMAL = 0
|
||||
WEATHER_NORMAL2 = 1
|
||||
WEATHER_RAINY = 2
|
||||
WEATHER_SNOWY = 3
|
||||
WEATHER_HEAVYRAIN = 4
|
||||
WEATHER_OTHER = 5
|
||||
SCENE_HIGHWAY = 0
|
||||
SCENE_URBAN = 1
|
||||
SCENE_RURAL = 2
|
||||
SCENE_TUNNEL = 3
|
||||
SCENE_OTHER = 5
|
||||
SCENE_CHARGESTATION = 4
|
||||
TIME_DAY = 0
|
||||
TIME_NIGHT = 1
|
||||
TIME_OTHER = 2
|
||||
LIGHT_NATRUALLIGHT = 0
|
||||
LIGHT_LAMPLIGHT = 1
|
||||
LIGHT_HARDLIGHT = 2
|
||||
LIGHT_LOWSUN = 3
|
||||
LIGHT_DARK = 4
|
||||
LIGHT_OTHER = 5
|
||||
WORK_CONDITION_WEATHER = 0
|
||||
WORK_CONDITION_LIGHT = 1
|
||||
WORK_CONDITION_SCENE = 2
|
||||
WORK_CONDITION_TIME = 3
|
||||
kShelterScenePartNone = 0
|
||||
kShelterSceneLeftTop = 1
|
||||
kShelterSceneRightTop = 2
|
||||
kShelterSceneLeftBottom = 4
|
||||
kShelterSceneRightBottom = 8
|
||||
kSceneNoShelter = 0
|
||||
kSceneShelter = 1
|
||||
kSceneQuarterShelter = 2
|
||||
kSceneHalfShelter = 3
|
||||
kSceneUrban = 0
|
||||
kSceneHighway = 1
|
||||
kSceneOpenAirParking = 2
|
||||
kSceneParking = 3
|
||||
kSceneTunnelEntrance = 4
|
||||
kSceneTunnel = 5
|
||||
kSceneTunnelExit = 6
|
||||
kSceneViaduct = 7
|
||||
kSceneFrontlighting = 0
|
||||
kSceneBacklighting = 1
|
||||
kSceneDry = 0
|
||||
kScenePonding = 1
|
||||
kSceneSnow = 2
|
||||
kSceneBumpy = 3
|
||||
kTerrainNormal = 0
|
||||
kTerrainGrass = 1
|
||||
kTerrainDesert = 2
|
||||
kTerrainWading = 3
|
||||
kTerrainRocks = 4
|
||||
kTerrainSnow = 5
|
||||
kTerrainMud = 6
|
||||
kSceneDay = 0
|
||||
kSceneNightWithStreetLamp = 1
|
||||
kSceneNightWithoutStreetLamp = 2
|
||||
|
||||
|
||||
|
||||
_SCENE = _descriptor.Descriptor(
|
||||
name='Scene',
|
||||
full_name='perception.Scene',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='shelter_cls', full_name='perception.Scene.shelter_cls', index=0,
|
||||
number=1, type=14, cpp_type=8, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='road_cls', full_name='perception.Scene.road_cls', index=1,
|
||||
number=2, type=14, cpp_type=8, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='lighting_dir_cls', full_name='perception.Scene.lighting_dir_cls', index=2,
|
||||
number=3, type=14, cpp_type=8, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='terrain_cls', full_name='perception.Scene.terrain_cls', index=3,
|
||||
number=4, type=14, cpp_type=8, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='lighting_cls', full_name='perception.Scene.lighting_cls', index=4,
|
||||
number=5, type=14, cpp_type=8, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='cam_id', full_name='perception.Scene.cam_id', index=5,
|
||||
number=6, type=11, cpp_type=10, label=1,
|
||||
has_default_value=False, default_value=None,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='ground_cls', full_name='perception.Scene.ground_cls', index=6,
|
||||
number=7, type=14, cpp_type=8, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='shelter_part', full_name='perception.Scene.shelter_part', index=7,
|
||||
number=8, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='frame_id', full_name='perception.Scene.frame_id', index=8,
|
||||
number=15, type=4, cpp_type=4, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='timestamp_us', full_name='perception.Scene.timestamp_us', index=9,
|
||||
number=16, type=4, cpp_type=4, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='tick_us', full_name='perception.Scene.tick_us', index=10,
|
||||
number=17, type=4, cpp_type=4, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
],
|
||||
serialized_options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto3',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=42,
|
||||
serialized_end=470,
|
||||
)
|
||||
|
||||
|
||||
_SCENELIST = _descriptor.Descriptor(
|
||||
name='SceneList',
|
||||
full_name='perception.SceneList',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='scene_list', full_name='perception.SceneList.scene_list', index=0,
|
||||
number=1, type=11, cpp_type=10, label=3,
|
||||
has_default_value=False, default_value=[],
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='lux', full_name='perception.SceneList.lux', index=1,
|
||||
number=2, type=2, cpp_type=6, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='ihbc_state', full_name='perception.SceneList.ihbc_state', index=2,
|
||||
number=3, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='frame_id', full_name='perception.SceneList.frame_id', index=3,
|
||||
number=6, type=4, cpp_type=4, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='timestamp_us', full_name='perception.SceneList.timestamp_us', index=4,
|
||||
number=7, type=4, cpp_type=4, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='tick_us', full_name='perception.SceneList.tick_us', index=5,
|
||||
number=8, type=4, cpp_type=4, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='debug_list', full_name='perception.SceneList.debug_list', index=6,
|
||||
number=9, type=11, cpp_type=10, label=3,
|
||||
has_default_value=False, default_value=[],
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
],
|
||||
serialized_options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto3',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=473,
|
||||
serialized_end=652,
|
||||
)
|
||||
|
||||
|
||||
_CAMERASCENE = _descriptor.Descriptor(
|
||||
name='CameraScene',
|
||||
full_name='perception.CameraScene',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='scene', full_name='perception.CameraScene.scene', index=0,
|
||||
number=1, type=11, cpp_type=10, label=1,
|
||||
has_default_value=False, default_value=None,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='camera_id', full_name='perception.CameraScene.camera_id', index=1,
|
||||
number=2, type=4, cpp_type=4, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='timestamp', full_name='perception.CameraScene.timestamp', index=2,
|
||||
number=4, type=4, cpp_type=4, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='tick', full_name='perception.CameraScene.tick', index=3,
|
||||
number=5, type=4, cpp_type=4, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
],
|
||||
serialized_options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto3',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=654,
|
||||
serialized_end=753,
|
||||
)
|
||||
|
||||
_SCENE.fields_by_name['shelter_cls'].enum_type = _SHELTERSCENETYPE
|
||||
_SCENE.fields_by_name['road_cls'].enum_type = _ROADSCENETYPE
|
||||
_SCENE.fields_by_name['lighting_dir_cls'].enum_type = _LIGHTINGDIRECTIONSCENETYPE
|
||||
_SCENE.fields_by_name['terrain_cls'].enum_type = _TERRAINTYPE
|
||||
_SCENE.fields_by_name['lighting_cls'].enum_type = _LIGHTINGSCENETYPE
|
||||
_SCENE.fields_by_name['cam_id'].message_type = camera__pb2._CAMID
|
||||
_SCENE.fields_by_name['ground_cls'].enum_type = _GROUNDSCENETYPE
|
||||
_SCENELIST.fields_by_name['scene_list'].message_type = _SCENE
|
||||
_SCENELIST.fields_by_name['debug_list'].message_type = _SCENE
|
||||
_CAMERASCENE.fields_by_name['scene'].message_type = _SCENE
|
||||
DESCRIPTOR.message_types_by_name['Scene'] = _SCENE
|
||||
DESCRIPTOR.message_types_by_name['SceneList'] = _SCENELIST
|
||||
DESCRIPTOR.message_types_by_name['CameraScene'] = _CAMERASCENE
|
||||
DESCRIPTOR.enum_types_by_name['WeatherType'] = _WEATHERTYPE
|
||||
DESCRIPTOR.enum_types_by_name['SceneType'] = _SCENETYPE
|
||||
DESCRIPTOR.enum_types_by_name['TimeType'] = _TIMETYPE
|
||||
DESCRIPTOR.enum_types_by_name['LightType'] = _LIGHTTYPE
|
||||
DESCRIPTOR.enum_types_by_name['WorkConditionType'] = _WORKCONDITIONTYPE
|
||||
DESCRIPTOR.enum_types_by_name['ShelterScenepPart'] = _SHELTERSCENEPPART
|
||||
DESCRIPTOR.enum_types_by_name['ShelterSceneType'] = _SHELTERSCENETYPE
|
||||
DESCRIPTOR.enum_types_by_name['RoadSceneType'] = _ROADSCENETYPE
|
||||
DESCRIPTOR.enum_types_by_name['LightingDirectionSceneType'] = _LIGHTINGDIRECTIONSCENETYPE
|
||||
DESCRIPTOR.enum_types_by_name['GroundSceneType'] = _GROUNDSCENETYPE
|
||||
DESCRIPTOR.enum_types_by_name['TerrainType'] = _TERRAINTYPE
|
||||
DESCRIPTOR.enum_types_by_name['LightingSceneType'] = _LIGHTINGSCENETYPE
|
||||
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
|
||||
|
||||
Scene = _reflection.GeneratedProtocolMessageType('Scene', (_message.Message,), {
|
||||
'DESCRIPTOR' : _SCENE,
|
||||
'__module__' : 'scene_pb2'
|
||||
# @@protoc_insertion_point(class_scope:perception.Scene)
|
||||
})
|
||||
_sym_db.RegisterMessage(Scene)
|
||||
|
||||
SceneList = _reflection.GeneratedProtocolMessageType('SceneList', (_message.Message,), {
|
||||
'DESCRIPTOR' : _SCENELIST,
|
||||
'__module__' : 'scene_pb2'
|
||||
# @@protoc_insertion_point(class_scope:perception.SceneList)
|
||||
})
|
||||
_sym_db.RegisterMessage(SceneList)
|
||||
|
||||
CameraScene = _reflection.GeneratedProtocolMessageType('CameraScene', (_message.Message,), {
|
||||
'DESCRIPTOR' : _CAMERASCENE,
|
||||
'__module__' : 'scene_pb2'
|
||||
# @@protoc_insertion_point(class_scope:perception.CameraScene)
|
||||
})
|
||||
_sym_db.RegisterMessage(CameraScene)
|
||||
|
||||
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
2010
tools/convert_merge_tracking_bundle/pyproto/tsr_define_pb2.py
Executable file
2010
tools/convert_merge_tracking_bundle/pyproto/tsr_define_pb2.py
Executable file
File diff suppressed because one or more lines are too long
720
tools/convert_merge_tracking_bundle/pyproto/vehicle_signal_pb2.py
Executable file
720
tools/convert_merge_tracking_bundle/pyproto/vehicle_signal_pb2.py
Executable file
@@ -0,0 +1,720 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: vehicle_signal.proto
|
||||
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
from google.protobuf import reflection as _reflection
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor.FileDescriptor(
|
||||
name='vehicle_signal.proto',
|
||||
package='minieye',
|
||||
syntax='proto3',
|
||||
serialized_options=None,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
serialized_pb=b'\n\x14vehicle_signal.proto\x12\x07minieye\"\xb1\x13\n\rVehicleSignal\x12\x34\n\x07signals\x18\x01 \x03(\x0b\x32#.minieye.VehicleSignal.SignalsEntry\x12\x11\n\ttimestamp\x18\x02 \x01(\x04\x12\x0c\n\x04tick\x18\x03 \x01(\x04\x1a.\n\x0cSignalsEntry\x12\x0b\n\x03key\x18\x01 \x01(\x05\x12\r\n\x05value\x18\x02 \x01(\x02:\x02\x38\x01\"\x98\x12\n\nSignalType\x12\r\n\tkCanSpeed\x10\x00\x12\r\n\tkTurnLamp\x10\x01\x12\x16\n\x12kHazardWarningLamp\x10\x02\x12\x0c\n\x08kBraking\x10\x03\x12\x10\n\x0ckAccelerator\x10\x04\x12\x17\n\x13kSteeringWheelAngle\x10\x05\x12\x1b\n\x17kSteeringWheelAngleRate\x10\x06\x12\x0f\n\x0bkFrontWiper\x10\x07\x12\x0e\n\nkDriveMode\x10\x08\x12\x0e\n\nkMovingDir\x10\t\x12\x0c\n\x08kYawRate\x10\n\x12\r\n\tkGearMode\x10\x0b\x12\x18\n\x14kABSFullyOperational\x10\x0c\x12\x16\n\x12kBrakPedalPosition\x10\r\x12\x16\n\x12kLongiAcceleration\x10\x0e\x12\x18\n\x14kLateralAcceleration\x10\x0f\x12\x19\n\x15kACUCollisionDetected\x10\x10\x12\x14\n\x10kAutoLightSwitch\x10\x11\x12\x12\n\x0ekLowBeanStatus\x10\x12\x12\x0e\n\nkESCStatus\x10\x13\x12\x15\n\x11kFrontWiperHighSt\x10\x14\x12\x10\n\x0ckHBeanStatus\x10\x15\x12\x10\n\x0ckTcsActiveSt\x10\x16\x12\x10\n\x0ckVdcActiveSt\x10\x17\x12\x0b\n\x07kEngSpd\x10\x18\x12\x0f\n\x0bkRLWheelSpd\x10\x19\x12\x12\n\x0ekRLWheelRotate\x10\x1a\x12\x0f\n\x0bkRRWheelSpd\x10\x1b\x12\x12\n\x0ekRRWheelRotate\x10\x1c\x12\x0f\n\x0bkFLWheelSpd\x10\x1d\x12\x12\n\x0ekFLWheelRotate\x10\x1e\x12\x0f\n\x0bkFRWheelSpd\x10\x1f\x12\x12\n\x0ekFRWheelRotate\x10 \x12\x11\n\rkStrngWhlTorq\x10!\x12\x13\n\x0fkSteeringModeSt\x10\"\x12\x12\n\x0ekHandsOffdetSt\x10#\x12\x11\n\rkEpsLkaCtrlSt\x10$\x12\x0e\n\nkAccCtrlSt\x10%\x12\x11\n\rkAccTargetSpd\x10&\x12\x11\n\rkLkaTorqueReq\x10\'\x12\x13\n\x0fkLkaTorqueReqSt\x10(\x12\x0f\n\x0bkEmsEngTorq\x10)\x12\x1c\n\x18kFrontLeftFogLightStatus\x10*\x12\x1d\n\x19kFrontRightFogLightStatus\x10+\x12\x17\n\x13kRearFogLightStatus\x10,\x12\x18\n\x14kRLWheelPulseCounter\x10-\x12\x18\n\x14kRRWheelPulseCounter\x10.\x12\x18\n\x14kFLWheelPulseCounter\x10/\x12\x18\n\x14kFRWheelPulseCounter\x10\x30\x12\x11\n\rkDriverDoorSt\x10\x31\x12\x14\n\x10kPassengerDoorSt\x10\x32\x12\x0e\n\nkRHRDoorSt\x10\x33\x12\x0e\n\nkLHRDoorSt\x10\x34\x12\x15\n\x11kDriverSeatBeltSw\x10\x35\x12\x16\n\x12kAutoParkingActive\x10\x36\x12\x12\n\x0ekCarEngineHood\x10\x37\x12\x17\n\x13kCarBackCompartment\x10\x38\x12\x17\n\x13kCarWindowLeftFront\x10\x39\x12\x18\n\x14kCarWindowRightFront\x10:\x12\x16\n\x12kCarWindowLeftRear\x10;\x12\x17\n\x13kCarWindowRightRear\x10<\x12\x18\n\x14kDisplayVehicleSpeed\x10=\x12\x13\n\x0fkHeadLampSwitch\x10>\x12\x12\n\x0ekFoldMirrorsSt\x10?\x12\"\n\x1ekParkingStationMonitorsCountdo\x10@\x12\x12\n\x0ekSupplyVoltage\x10\x41\x12\x11\n\rkTurnlightSts\x10\x42\x12\x14\n\x10kSeatOccupiedSts\x10\x43\x12\x13\n\x0fkSuspCurrentLvl\x10\x44\x12\x15\n\x11kVehicleDriveMode\x10\x45\x12\x0c\n\x08kCarMode\x10\x46\x12\r\n\tkCCOSwSts\x10G\x12\x0f\n\x0bkTrailerSts\x10H\x12\x15\n\x11kChargeConnectSts\x10I\x12\x0e\n\nkPowerMode\x10J\x12\x0c\n\x08kHVReady\x10K\x12\x0f\n\x0bkVcuPtReady\x10L\x12\x0b\n\x07kSOCBms\x10M\x12\x1f\n\x1bkTirePressureWarningLampSts\x10N\x12\x12\n\x0ekAmbBrightness\x10O\x12\r\n\tkRainfall\x10P\x12\x19\n\x15kExternalTemperatureC\x10Q\x12\x18\n\x14kDriveDrosinessLevel\x10R\x12\x0c\n\x08kUTCYear\x10S\x12\r\n\tkUTCMonth\x10T\x12\x0b\n\x07kUTCDay\x10U\x12\x0c\n\x08kUTCHour\x10V\x12\x0e\n\nkUTCMinute\x10W\x12\x0e\n\nkUTCSecond\x10X\x12\x0f\n\x0bkHDCCtrlSts\x10Y\x12\x0e\n\nkABSActive\x10Z\x12\x0e\n\nkCDPActive\x10[\x12\x1d\n\x19kPilotLonAccCtrlAvailable\x10\\\x12\x19\n\x15kPilotLonAccCtrActive\x10]\x12\x1d\n\x19kPilotLonDecCtrlAvailable\x10^\x12\x19\n\x15kPilotLonDecCtrActive\x10_\x12\x1a\n\x16kPilotLatCtrlAvailable\x10`\x12\x16\n\x12kPilotLatCtrActive\x10\x61\x12\x15\n\x11kDrvrAccrPedlOvrd\x10\x62\x12\x0f\n\x0bkEpsFailSts\x10\x63\x12\x12\n\x0ekSteerDrvrOvrd\x10\x64\x12\x1b\n\x17kDisplayVehicleSpeedKph\x10\x65\x12\x14\n\x10kTotalOdometerKm\x10\x66\x12\x0e\n\nkEPBStatus\x10g\x12\x0e\n\nkEPBErrSts\x10h\x12\r\n\tkSetSpdUp\x10i\x12\r\n\tkSetSpdDw\x10j\x12\x0e\n\nkTimeGapUp\x10k\x12\x0e\n\nkTimeGapDw\x10l\x12\x0f\n\x0bkCruiseCtrl\x10m\x12\x0b\n\x07kCancel\x10n\x12\x0c\n\x08kMfsCust\x10o\x12\x0f\n\x0bkIccCustBtn\x10p\x12\x12\n\x0ekTurnSigLvrCmd\x10qb\x06proto3'
|
||||
)
|
||||
|
||||
|
||||
|
||||
_VEHICLESIGNAL_SIGNALTYPE = _descriptor.EnumDescriptor(
|
||||
name='SignalType',
|
||||
full_name='minieye.VehicleSignal.SignalType',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
values=[
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kCanSpeed', index=0, number=0,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kTurnLamp', index=1, number=1,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kHazardWarningLamp', index=2, number=2,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kBraking', index=3, number=3,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kAccelerator', index=4, number=4,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSteeringWheelAngle', index=5, number=5,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSteeringWheelAngleRate', index=6, number=6,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kFrontWiper', index=7, number=7,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kDriveMode', index=8, number=8,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kMovingDir', index=9, number=9,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kYawRate', index=10, number=10,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kGearMode', index=11, number=11,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kABSFullyOperational', index=12, number=12,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kBrakPedalPosition', index=13, number=13,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kLongiAcceleration', index=14, number=14,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kLateralAcceleration', index=15, number=15,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kACUCollisionDetected', index=16, number=16,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kAutoLightSwitch', index=17, number=17,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kLowBeanStatus', index=18, number=18,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kESCStatus', index=19, number=19,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kFrontWiperHighSt', index=20, number=20,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kHBeanStatus', index=21, number=21,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kTcsActiveSt', index=22, number=22,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kVdcActiveSt', index=23, number=23,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kEngSpd', index=24, number=24,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kRLWheelSpd', index=25, number=25,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kRLWheelRotate', index=26, number=26,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kRRWheelSpd', index=27, number=27,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kRRWheelRotate', index=28, number=28,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kFLWheelSpd', index=29, number=29,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kFLWheelRotate', index=30, number=30,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kFRWheelSpd', index=31, number=31,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kFRWheelRotate', index=32, number=32,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kStrngWhlTorq', index=33, number=33,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSteeringModeSt', index=34, number=34,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kHandsOffdetSt', index=35, number=35,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kEpsLkaCtrlSt', index=36, number=36,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kAccCtrlSt', index=37, number=37,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kAccTargetSpd', index=38, number=38,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kLkaTorqueReq', index=39, number=39,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kLkaTorqueReqSt', index=40, number=40,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kEmsEngTorq', index=41, number=41,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kFrontLeftFogLightStatus', index=42, number=42,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kFrontRightFogLightStatus', index=43, number=43,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kRearFogLightStatus', index=44, number=44,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kRLWheelPulseCounter', index=45, number=45,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kRRWheelPulseCounter', index=46, number=46,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kFLWheelPulseCounter', index=47, number=47,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kFRWheelPulseCounter', index=48, number=48,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kDriverDoorSt', index=49, number=49,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kPassengerDoorSt', index=50, number=50,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kRHRDoorSt', index=51, number=51,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kLHRDoorSt', index=52, number=52,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kDriverSeatBeltSw', index=53, number=53,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kAutoParkingActive', index=54, number=54,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kCarEngineHood', index=55, number=55,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kCarBackCompartment', index=56, number=56,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kCarWindowLeftFront', index=57, number=57,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kCarWindowRightFront', index=58, number=58,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kCarWindowLeftRear', index=59, number=59,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kCarWindowRightRear', index=60, number=60,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kDisplayVehicleSpeed', index=61, number=61,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kHeadLampSwitch', index=62, number=62,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kFoldMirrorsSt', index=63, number=63,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kParkingStationMonitorsCountdo', index=64, number=64,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSupplyVoltage', index=65, number=65,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kTurnlightSts', index=66, number=66,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSeatOccupiedSts', index=67, number=67,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSuspCurrentLvl', index=68, number=68,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kVehicleDriveMode', index=69, number=69,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kCarMode', index=70, number=70,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kCCOSwSts', index=71, number=71,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kTrailerSts', index=72, number=72,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kChargeConnectSts', index=73, number=73,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kPowerMode', index=74, number=74,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kHVReady', index=75, number=75,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kVcuPtReady', index=76, number=76,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSOCBms', index=77, number=77,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kTirePressureWarningLampSts', index=78, number=78,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kAmbBrightness', index=79, number=79,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kRainfall', index=80, number=80,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kExternalTemperatureC', index=81, number=81,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kDriveDrosinessLevel', index=82, number=82,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kUTCYear', index=83, number=83,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kUTCMonth', index=84, number=84,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kUTCDay', index=85, number=85,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kUTCHour', index=86, number=86,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kUTCMinute', index=87, number=87,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kUTCSecond', index=88, number=88,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kHDCCtrlSts', index=89, number=89,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kABSActive', index=90, number=90,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kCDPActive', index=91, number=91,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kPilotLonAccCtrlAvailable', index=92, number=92,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kPilotLonAccCtrActive', index=93, number=93,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kPilotLonDecCtrlAvailable', index=94, number=94,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kPilotLonDecCtrActive', index=95, number=95,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kPilotLatCtrlAvailable', index=96, number=96,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kPilotLatCtrActive', index=97, number=97,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kDrvrAccrPedlOvrd', index=98, number=98,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kEpsFailSts', index=99, number=99,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSteerDrvrOvrd', index=100, number=100,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kDisplayVehicleSpeedKph', index=101, number=101,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kTotalOdometerKm', index=102, number=102,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kEPBStatus', index=103, number=103,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kEPBErrSts', index=104, number=104,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSetSpdUp', index=105, number=105,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kSetSpdDw', index=106, number=106,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kTimeGapUp', index=107, number=107,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kTimeGapDw', index=108, number=108,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kCruiseCtrl', index=109, number=109,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kCancel', index=110, number=110,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kMfsCust', index=111, number=111,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kIccCustBtn', index=112, number=112,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='kTurnSigLvrCmd', index=113, number=113,
|
||||
serialized_options=None,
|
||||
type=None,
|
||||
create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
containing_type=None,
|
||||
serialized_options=None,
|
||||
serialized_start=187,
|
||||
serialized_end=2515,
|
||||
)
|
||||
_sym_db.RegisterEnumDescriptor(_VEHICLESIGNAL_SIGNALTYPE)
|
||||
|
||||
|
||||
_VEHICLESIGNAL_SIGNALSENTRY = _descriptor.Descriptor(
|
||||
name='SignalsEntry',
|
||||
full_name='minieye.VehicleSignal.SignalsEntry',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='key', full_name='minieye.VehicleSignal.SignalsEntry.key', index=0,
|
||||
number=1, type=5, cpp_type=1, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='value', full_name='minieye.VehicleSignal.SignalsEntry.value', index=1,
|
||||
number=2, type=2, cpp_type=6, label=1,
|
||||
has_default_value=False, default_value=float(0),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
],
|
||||
serialized_options=b'8\001',
|
||||
is_extendable=False,
|
||||
syntax='proto3',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=138,
|
||||
serialized_end=184,
|
||||
)
|
||||
|
||||
_VEHICLESIGNAL = _descriptor.Descriptor(
|
||||
name='VehicleSignal',
|
||||
full_name='minieye.VehicleSignal',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
create_key=_descriptor._internal_create_key,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='signals', full_name='minieye.VehicleSignal.signals', index=0,
|
||||
number=1, type=11, cpp_type=10, label=3,
|
||||
has_default_value=False, default_value=[],
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='timestamp', full_name='minieye.VehicleSignal.timestamp', index=1,
|
||||
number=2, type=4, cpp_type=4, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='tick', full_name='minieye.VehicleSignal.tick', index=2,
|
||||
number=3, type=4, cpp_type=4, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[_VEHICLESIGNAL_SIGNALSENTRY, ],
|
||||
enum_types=[
|
||||
_VEHICLESIGNAL_SIGNALTYPE,
|
||||
],
|
||||
serialized_options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto3',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=34,
|
||||
serialized_end=2515,
|
||||
)
|
||||
|
||||
_VEHICLESIGNAL_SIGNALSENTRY.containing_type = _VEHICLESIGNAL
|
||||
_VEHICLESIGNAL.fields_by_name['signals'].message_type = _VEHICLESIGNAL_SIGNALSENTRY
|
||||
_VEHICLESIGNAL_SIGNALTYPE.containing_type = _VEHICLESIGNAL
|
||||
DESCRIPTOR.message_types_by_name['VehicleSignal'] = _VEHICLESIGNAL
|
||||
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
|
||||
|
||||
VehicleSignal = _reflection.GeneratedProtocolMessageType('VehicleSignal', (_message.Message,), {
|
||||
|
||||
'SignalsEntry' : _reflection.GeneratedProtocolMessageType('SignalsEntry', (_message.Message,), {
|
||||
'DESCRIPTOR' : _VEHICLESIGNAL_SIGNALSENTRY,
|
||||
'__module__' : 'vehicle_signal_pb2'
|
||||
# @@protoc_insertion_point(class_scope:minieye.VehicleSignal.SignalsEntry)
|
||||
})
|
||||
,
|
||||
'DESCRIPTOR' : _VEHICLESIGNAL,
|
||||
'__module__' : 'vehicle_signal_pb2'
|
||||
# @@protoc_insertion_point(class_scope:minieye.VehicleSignal)
|
||||
})
|
||||
_sym_db.RegisterMessage(VehicleSignal)
|
||||
_sym_db.RegisterMessage(VehicleSignal.SignalsEntry)
|
||||
|
||||
|
||||
_VEHICLESIGNAL_SIGNALSENTRY._options = None
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
1255
tools/convert_merge_tracking_bundle/pyproto/vehicle_signal_v2_pb2.py
Executable file
1255
tools/convert_merge_tracking_bundle/pyproto/vehicle_signal_v2_pb2.py
Executable file
File diff suppressed because one or more lines are too long
376
tools/copy_detection2d_g1m3_files.py
Executable file
376
tools/copy_detection2d_g1m3_files.py
Executable file
@@ -0,0 +1,376 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import re
|
||||
import shutil
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
from PIL import Image
|
||||
|
||||
|
||||
DEFAULT_SRC_ROOT = Path("/mnt/nfs/mono3d/ydong_data/Detection/2Ddetection_20260402")
|
||||
DEFAULT_DST_IMAGES_DIR = Path(
|
||||
"/mnt/nfs/mono3d/xdzhu_data/Mono3d/Mono3d_4face_2m_g1m3/driving_png_20260202/Detection2D/D4Q_2D/images"
|
||||
)
|
||||
DEFAULT_DST_LABELS_DIR = Path(
|
||||
"/mnt/nfs/mono3d/xdzhu_data/Mono3d/Mono3d_4face_2m_g1m3/driving_png_20260320/Detection2D/D4Q/labels"
|
||||
)
|
||||
IMAGE_SUFFIXES = {".jpg", ".jpeg"}
|
||||
LABEL_SUFFIXES = {".txt"}
|
||||
DEFAULT_NAME_TOKENS = ("D01", "D4Q", "D4Q2", "D01P")
|
||||
DEFAULT_SOURCE_SIZE = (3840, 2160)
|
||||
DEFAULT_TARGET_SIZE = (1920, 1080)
|
||||
|
||||
try:
|
||||
RESAMPLE_LANCZOS = Image.Resampling.LANCZOS
|
||||
except AttributeError: # pragma: no cover - Pillow < 9.1 compatibility
|
||||
RESAMPLE_LANCZOS = Image.LANCZOS
|
||||
|
||||
|
||||
def parse_size(value: str) -> tuple[int, int]:
|
||||
match = re.fullmatch(r"\s*(\d+)\s*[xX]\s*(\d+)\s*", value)
|
||||
if match is None:
|
||||
raise argparse.ArgumentTypeError(f"Invalid size '{value}'. Expected WIDTHxHEIGHT, e.g. 3840x2160.")
|
||||
|
||||
width, height = (int(group) for group in match.groups())
|
||||
if width <= 0 or height <= 0:
|
||||
raise argparse.ArgumentTypeError(f"Image size must be positive, got {value}.")
|
||||
return width, height
|
||||
|
||||
|
||||
def parse_name_tokens(raw_tokens: Iterable[str] | None) -> tuple[str, ...]:
|
||||
if raw_tokens is None:
|
||||
return DEFAULT_NAME_TOKENS
|
||||
|
||||
tokens: list[str] = []
|
||||
for raw_token in raw_tokens:
|
||||
tokens.extend(piece.strip() for piece in raw_token.split(","))
|
||||
|
||||
normalized = tuple(dict.fromkeys(token for token in tokens if token))
|
||||
if not normalized:
|
||||
raise ValueError("At least one non-empty --name-token value is required.")
|
||||
return normalized
|
||||
|
||||
|
||||
def build_name_pattern(name_tokens: tuple[str, ...]) -> re.Pattern[str]:
|
||||
alternatives = "|".join(re.escape(token.lower()) for token in sorted(name_tokens, key=len, reverse=True))
|
||||
# Match dataset tokens as standalone parts of the file name so strings like "...17b78d01..."
|
||||
# are not treated as a valid D01 sample.
|
||||
return re.compile(rf"(^|[^0-9a-z])(?:{alternatives})(?=[^0-9a-z]|$)", re.IGNORECASE)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Copy Detection2D images whose names match the requested tokens, resize the image, "
|
||||
"and copy the paired labels."
|
||||
)
|
||||
)
|
||||
parser.add_argument("--src-root", type=Path, default=DEFAULT_SRC_ROOT, help="Source Detection2D dataset root.")
|
||||
parser.add_argument(
|
||||
"--dst-images-dir",
|
||||
type=Path,
|
||||
default=DEFAULT_DST_IMAGES_DIR,
|
||||
help="Destination directory for resized image files.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dst-labels-dir",
|
||||
type=Path,
|
||||
default=DEFAULT_DST_LABELS_DIR,
|
||||
help="Destination directory for paired label files.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--name-token",
|
||||
action="append",
|
||||
dest="name_tokens",
|
||||
default=None,
|
||||
help=(
|
||||
"Case-insensitive dataset token to match in the file name. Repeat the flag or provide "
|
||||
"a comma-separated list. Defaults to D01, D4Q, D4Q2, D01P."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--source-size",
|
||||
type=parse_size,
|
||||
default=DEFAULT_SOURCE_SIZE,
|
||||
help="Only process images with this source size (WIDTHxHEIGHT).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--target-size",
|
||||
type=parse_size,
|
||||
default=DEFAULT_TARGET_SIZE,
|
||||
help="Resize matching images to this size (WIDTHxHEIGHT).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--overwrite",
|
||||
action="store_true",
|
||||
help="Overwrite destination files if they already exist.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--limit",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Only inspect the first N matching image names.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--log-every",
|
||||
type=int,
|
||||
default=1000,
|
||||
help="Log progress every N inspected image files.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Print planned work without writing files.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def iter_matching_images(
|
||||
src_images_dir: Path,
|
||||
suffixes: set[str],
|
||||
name_pattern: re.Pattern[str],
|
||||
limit: int | None = None,
|
||||
) -> list[Path]:
|
||||
matches: list[Path] = []
|
||||
for path in src_images_dir.rglob("*"):
|
||||
if not path.is_file() or path.suffix.lower() not in suffixes:
|
||||
continue
|
||||
if not name_pattern.search(path.stem.lower()):
|
||||
continue
|
||||
|
||||
matches.append(path)
|
||||
if limit is not None and len(matches) >= limit:
|
||||
break
|
||||
|
||||
return matches
|
||||
|
||||
|
||||
def read_image_size(image_path: Path) -> tuple[int, int]:
|
||||
with Image.open(image_path) as image:
|
||||
return image.size
|
||||
|
||||
|
||||
def resize_one_image(
|
||||
src_path: Path,
|
||||
dst_dir: Path,
|
||||
target_size: tuple[int, int],
|
||||
overwrite: bool,
|
||||
dry_run: bool,
|
||||
) -> tuple[str, Path, str]:
|
||||
dst_path = dst_dir / src_path.name
|
||||
|
||||
if dst_path.exists() and not overwrite:
|
||||
return "skipped", dst_path, ""
|
||||
|
||||
if dry_run:
|
||||
return "planned", dst_path, ""
|
||||
|
||||
dst_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
with Image.open(src_path) as image:
|
||||
if image.mode not in {"RGB", "L"}:
|
||||
image = image.convert("RGB")
|
||||
resized = image.resize(target_size, RESAMPLE_LANCZOS)
|
||||
save_kwargs = {"quality": 95} if dst_path.suffix.lower() in {".jpg", ".jpeg"} else {}
|
||||
resized.save(dst_path, **save_kwargs)
|
||||
except OSError as exc:
|
||||
return "failed", dst_path, str(exc)
|
||||
|
||||
return "copied", dst_path, ""
|
||||
|
||||
|
||||
def copy_one_label(src_path: Path, dst_dir: Path, overwrite: bool, dry_run: bool) -> tuple[str, Path, str]:
|
||||
dst_path = dst_dir / src_path.name
|
||||
|
||||
if dst_path.exists() and not overwrite:
|
||||
return "skipped", dst_path, ""
|
||||
|
||||
if dry_run:
|
||||
return "planned", dst_path, ""
|
||||
|
||||
dst_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
shutil.copy2(src_path, dst_path)
|
||||
except OSError as exc:
|
||||
return "failed", dst_path, str(exc)
|
||||
|
||||
return "copied", dst_path, ""
|
||||
|
||||
|
||||
def maybe_log_plan(image_path: Path, label_path: Path, dst_images_dir: Path, dst_labels_dir: Path, index: int) -> None:
|
||||
if index >= 5:
|
||||
return
|
||||
|
||||
logging.info("plan image: %s -> %s", image_path, dst_images_dir / image_path.name)
|
||||
logging.info("plan label: %s -> %s", label_path, dst_labels_dir / label_path.name)
|
||||
|
||||
|
||||
def log_progress(index: int, total: int, counts: Counter) -> None:
|
||||
logging.info(
|
||||
(
|
||||
"progress %d/%d | matched=%d wrong_size=%d missing_label=%d "
|
||||
"images(copied=%d skipped=%d planned=%d failed=%d) "
|
||||
"labels(copied=%d skipped=%d planned=%d failed=%d)"
|
||||
),
|
||||
index,
|
||||
total,
|
||||
counts["pairs_matched"],
|
||||
counts["images_wrong_size"],
|
||||
counts["labels_missing"],
|
||||
counts["images_copied"],
|
||||
counts["images_skipped"],
|
||||
counts["images_planned"],
|
||||
counts["images_failed"],
|
||||
counts["labels_copied"],
|
||||
counts["labels_skipped"],
|
||||
counts["labels_planned"],
|
||||
counts["labels_failed"],
|
||||
)
|
||||
|
||||
|
||||
def process_dataset(
|
||||
image_paths: list[Path],
|
||||
src_labels_dir: Path,
|
||||
dst_images_dir: Path,
|
||||
dst_labels_dir: Path,
|
||||
source_size: tuple[int, int],
|
||||
target_size: tuple[int, int],
|
||||
overwrite: bool,
|
||||
dry_run: bool,
|
||||
log_every: int,
|
||||
) -> Counter:
|
||||
counts: Counter[str] = Counter()
|
||||
total = len(image_paths)
|
||||
|
||||
for index, image_path in enumerate(image_paths, start=1):
|
||||
label_path = src_labels_dir / f"{image_path.stem}.txt"
|
||||
if label_path.suffix.lower() not in LABEL_SUFFIXES:
|
||||
counts["labels_invalid_suffix"] += 1
|
||||
elif not label_path.exists():
|
||||
counts["labels_missing"] += 1
|
||||
logging.warning("missing label for image: %s", image_path)
|
||||
else:
|
||||
try:
|
||||
image_size = read_image_size(image_path)
|
||||
except OSError as exc:
|
||||
counts["images_failed"] += 1
|
||||
logging.error("failed to read image size: %s | %s", image_path, exc)
|
||||
else:
|
||||
if image_size != source_size:
|
||||
counts["images_wrong_size"] += 1
|
||||
if counts["images_wrong_size"] <= 10:
|
||||
logging.info("skip image with unexpected size %s: %s", image_size, image_path)
|
||||
else:
|
||||
counts["pairs_matched"] += 1
|
||||
if dry_run:
|
||||
maybe_log_plan(image_path, label_path, dst_images_dir, dst_labels_dir, counts["pairs_matched"] - 1)
|
||||
|
||||
image_status, dst_image_path, image_message = resize_one_image(
|
||||
src_path=image_path,
|
||||
dst_dir=dst_images_dir,
|
||||
target_size=target_size,
|
||||
overwrite=overwrite,
|
||||
dry_run=dry_run,
|
||||
)
|
||||
counts[f"images_{image_status}"] += 1
|
||||
if image_status == "failed":
|
||||
logging.error("image write failed: %s -> %s | %s", image_path, dst_image_path, image_message)
|
||||
else:
|
||||
label_status, dst_label_path, label_message = copy_one_label(
|
||||
src_path=label_path,
|
||||
dst_dir=dst_labels_dir,
|
||||
overwrite=overwrite,
|
||||
dry_run=dry_run,
|
||||
)
|
||||
counts[f"labels_{label_status}"] += 1
|
||||
if label_status == "failed":
|
||||
logging.error("label copy failed: %s -> %s | %s", label_path, dst_label_path, label_message)
|
||||
|
||||
if index % log_every == 0 or index == total:
|
||||
log_progress(index, total, counts)
|
||||
|
||||
return counts
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s")
|
||||
|
||||
src_root = args.src_root.resolve()
|
||||
src_images_dir = src_root / "images"
|
||||
src_labels_dir = src_root / "labels"
|
||||
dst_images_dir = args.dst_images_dir.resolve()
|
||||
dst_labels_dir = args.dst_labels_dir.resolve()
|
||||
name_tokens = parse_name_tokens(args.name_tokens)
|
||||
name_pattern = build_name_pattern(name_tokens)
|
||||
|
||||
if not src_root.exists():
|
||||
raise FileNotFoundError(f"Source root does not exist: {src_root}")
|
||||
if not src_images_dir.exists():
|
||||
raise FileNotFoundError(f"Source images directory does not exist: {src_images_dir}")
|
||||
if not src_labels_dir.exists():
|
||||
raise FileNotFoundError(f"Source labels directory does not exist: {src_labels_dir}")
|
||||
if args.log_every <= 0:
|
||||
raise ValueError("--log-every must be a positive integer.")
|
||||
if args.limit is not None and args.limit <= 0:
|
||||
raise ValueError("--limit must be a positive integer when provided.")
|
||||
|
||||
image_paths = iter_matching_images(src_images_dir, IMAGE_SUFFIXES, name_pattern, limit=args.limit)
|
||||
|
||||
logging.info("source root: %s", src_root)
|
||||
logging.info("destination images dir: %s", dst_images_dir)
|
||||
logging.info("destination labels dir: %s", dst_labels_dir)
|
||||
logging.info("file name tokens: %s", ", ".join(name_tokens))
|
||||
logging.info("required source size: %sx%s", *args.source_size)
|
||||
logging.info("target resize: %sx%s", *args.target_size)
|
||||
logging.info(
|
||||
"labels are copied without rewriting because the Detection2D labels are normalized; "
|
||||
"a full-image resize keeps normalized coordinates unchanged."
|
||||
)
|
||||
logging.info("found %d candidate image(s) by name", len(image_paths))
|
||||
|
||||
counts = process_dataset(
|
||||
image_paths=image_paths,
|
||||
src_labels_dir=src_labels_dir,
|
||||
dst_images_dir=dst_images_dir,
|
||||
dst_labels_dir=dst_labels_dir,
|
||||
source_size=args.source_size,
|
||||
target_size=args.target_size,
|
||||
overwrite=args.overwrite,
|
||||
dry_run=args.dry_run,
|
||||
log_every=args.log_every,
|
||||
)
|
||||
|
||||
if counts["images_failed"] or counts["labels_failed"]:
|
||||
logging.error(
|
||||
"copy finished with failures | image_failed=%d label_failed=%d",
|
||||
counts["images_failed"],
|
||||
counts["labels_failed"],
|
||||
)
|
||||
return 1
|
||||
|
||||
logging.info(
|
||||
(
|
||||
"copy finished successfully | matched=%d wrong_size=%d missing_label=%d "
|
||||
"images(copied=%d skipped=%d planned=%d) labels(copied=%d skipped=%d planned=%d)"
|
||||
),
|
||||
counts["pairs_matched"],
|
||||
counts["images_wrong_size"],
|
||||
counts["labels_missing"],
|
||||
counts["images_copied"],
|
||||
counts["images_skipped"],
|
||||
counts["images_planned"],
|
||||
counts["labels_copied"],
|
||||
counts["labels_skipped"],
|
||||
counts["labels_planned"],
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
163
tools/data_mining/batch_convert_txt_to_json.py
Executable file
163
tools/data_mining/batch_convert_txt_to_json.py
Executable file
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Batch convert ground truth txt label files to JSON format.
|
||||
|
||||
Usage:
|
||||
python test_scripts/batch_convert_txt_to_json.py <input_dir> <output_dir> [options]
|
||||
|
||||
Examples:
|
||||
# Convert all txt files in a directory
|
||||
python test_scripts/batch_convert_txt_to_json.py /path/to/labels/ /path/to/labels_json/
|
||||
|
||||
# With custom image dimensions
|
||||
python test_scripts/batch_convert_txt_to_json.py /path/to/labels/ /path/to/labels_json/ \\
|
||||
--image-width 1920 --image-height 1080
|
||||
|
||||
# Recursive conversion (preserving subdirectory structure)
|
||||
python test_scripts/batch_convert_txt_to_json.py /path/to/labels/ /path/to/labels_json/ --recursive
|
||||
"""
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
# Allow importing from the same test_scripts directory
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from convert_txt_to_json import convert_txt_to_json
|
||||
|
||||
|
||||
def batch_convert(input_dir, output_dir, img_width=1920, img_height=1080,
|
||||
recursive=False, workers=4, overwrite=False):
|
||||
"""
|
||||
Batch convert all txt label files in input_dir to JSON format.
|
||||
|
||||
Args:
|
||||
input_dir: Path to directory containing .txt label files
|
||||
output_dir: Path to directory where .json files will be saved
|
||||
img_width: Image width for coordinate denormalization
|
||||
img_height: Image height for coordinate denormalization
|
||||
recursive: If True, search subdirectories recursively
|
||||
workers: Number of parallel worker threads
|
||||
overwrite: If True, overwrite existing JSON files
|
||||
"""
|
||||
input_path = Path(input_dir)
|
||||
output_path = Path(output_dir)
|
||||
|
||||
if not input_path.exists():
|
||||
raise FileNotFoundError(f"Input directory not found: {input_dir}")
|
||||
|
||||
# Collect all .txt files
|
||||
pattern = "**/*.txt" if recursive else "*.txt"
|
||||
txt_files = sorted(input_path.glob(pattern))
|
||||
|
||||
if not txt_files:
|
||||
print(f"No .txt files found in: {input_dir}")
|
||||
return
|
||||
|
||||
print(f"Found {len(txt_files)} txt file(s) in: {input_dir}")
|
||||
print(f"Output directory: {output_dir}")
|
||||
print(f"Image dimensions: {img_width}x{img_height}")
|
||||
print(f"Workers: {workers}, Recursive: {recursive}, Overwrite: {overwrite}")
|
||||
print("-" * 60)
|
||||
|
||||
# Build list of (src, dst) pairs
|
||||
tasks = []
|
||||
skipped = 0
|
||||
for txt_file in txt_files:
|
||||
# Preserve relative subdirectory structure when recursive
|
||||
rel = txt_file.relative_to(input_path)
|
||||
json_file = output_path / rel.with_suffix(".json")
|
||||
|
||||
if json_file.exists() and not overwrite:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
json_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
tasks.append((txt_file, json_file))
|
||||
|
||||
if skipped:
|
||||
print(f"Skipping {skipped} already-converted file(s) (use --overwrite to force).")
|
||||
|
||||
if not tasks:
|
||||
print("Nothing to do.")
|
||||
return
|
||||
|
||||
# Process files (parallel when workers > 1)
|
||||
success = 0
|
||||
failed = 0
|
||||
errors = []
|
||||
|
||||
def _convert(args):
|
||||
src, dst = args
|
||||
convert_txt_to_json(str(src), str(dst), img_width, img_height)
|
||||
return str(src)
|
||||
|
||||
if workers > 1:
|
||||
with ThreadPoolExecutor(max_workers=workers) as executor:
|
||||
futures = {executor.submit(_convert, t): t for t in tasks}
|
||||
for i, future in enumerate(as_completed(futures), 1):
|
||||
src, _ = futures[future]
|
||||
try:
|
||||
future.result()
|
||||
success += 1
|
||||
if i % 100 == 0 or i == len(tasks):
|
||||
print(f" [{i}/{len(tasks)}] done")
|
||||
except Exception as e:
|
||||
failed += 1
|
||||
errors.append((str(src), str(e)))
|
||||
print(f" ERROR: {src} -> {e}")
|
||||
else:
|
||||
for i, (src, dst) in enumerate(tasks, 1):
|
||||
try:
|
||||
convert_txt_to_json(str(src), str(dst), img_width, img_height)
|
||||
success += 1
|
||||
if i % 100 == 0 or i == len(tasks):
|
||||
print(f" [{i}/{len(tasks)}] {src.name}")
|
||||
except Exception as e:
|
||||
failed += 1
|
||||
errors.append((str(src), str(e)))
|
||||
print(f" ERROR: {src} -> {e}")
|
||||
|
||||
print("-" * 60)
|
||||
print(f"Done. Success: {success}, Failed: {failed}, Skipped: {skipped}")
|
||||
if errors:
|
||||
print("\nFailed files:")
|
||||
for path, msg in errors:
|
||||
print(f" {path}: {msg}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Batch convert ground truth txt label files to JSON format",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=__doc__,
|
||||
)
|
||||
parser.add_argument("input_dir", help="Input directory containing .txt label files")
|
||||
parser.add_argument("output_dir", help="Output directory for .json files")
|
||||
parser.add_argument("--image-width", type=int, default=1920,
|
||||
help="Image width for coordinate denormalization (default: 1920)")
|
||||
parser.add_argument("--image-height", type=int, default=1080,
|
||||
help="Image height for coordinate denormalization (default: 1080)")
|
||||
parser.add_argument("--recursive", action="store_true",
|
||||
help="Recursively search subdirectories for txt files")
|
||||
parser.add_argument("--workers", type=int, default=4,
|
||||
help="Number of parallel worker threads (default: 4)")
|
||||
parser.add_argument("--overwrite", action="store_true",
|
||||
help="Overwrite existing JSON files (default: skip)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
batch_convert(
|
||||
input_dir=args.input_dir,
|
||||
output_dir=args.output_dir,
|
||||
img_width=args.image_width,
|
||||
img_height=args.image_height,
|
||||
recursive=args.recursive,
|
||||
workers=args.workers,
|
||||
overwrite=args.overwrite,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
316
tools/data_mining/convert_txt_to_json.py
Executable file
316
tools/data_mining/convert_txt_to_json.py
Executable file
@@ -0,0 +1,316 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Convert ground truth txt format to JSON format.
|
||||
|
||||
Usage:
|
||||
python convert_txt_to_json.py <input_txt_file> <output_json_file> [--image-width WIDTH] [--image-height HEIGHT]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
# from ultralytics.utils import YAML
|
||||
|
||||
# DEFAULT_DATA_CONFIG = Path(__file__).resolve().parents[2] / 'ultralytics' / 'cfg' / 'datasets' / 'mono3d_ground.yaml'
|
||||
DEFAULT_CLASS_MAP = {
|
||||
'car': 0,
|
||||
'suv': 1,
|
||||
'pickup': 2,
|
||||
'medium_car': 3,
|
||||
'van': 4,
|
||||
'bus': 5,
|
||||
'truck': 6,
|
||||
'tanker': 6,
|
||||
'large_truck': 6,
|
||||
'construction_vehicle': 6,
|
||||
'special_vehicle': 7,
|
||||
'unknown': 8,
|
||||
'pedestrian': 9,
|
||||
'bicyclist': 10,
|
||||
'motorcyclist': 10,
|
||||
'bicycle': 11,
|
||||
'motorcycle': 11,
|
||||
'tricycle': 12,
|
||||
'tricyclist': 12,
|
||||
'traffic_sign': 13,
|
||||
'wheel': 14,
|
||||
'plate': 15,
|
||||
'face': 16,
|
||||
}
|
||||
|
||||
cutcls_map = {
|
||||
0: 'nocut',
|
||||
1: 'cutin',
|
||||
2: 'cutout',
|
||||
}
|
||||
|
||||
EMPTY_3D_ORI = ["-1.0"] * 13
|
||||
EMPTY_3D_FACE = ["-1.0"] * 8
|
||||
|
||||
|
||||
def load_class_map(data_config_path: str | Path | None = None) -> dict[str, int]:
|
||||
"""Load class_map from dataset YAML, with a synced fallback for standalone use."""
|
||||
# config_path = Path(data_config_path).expanduser().resolve() if data_config_path else DEFAULT_DATA_CONFIG
|
||||
# if config_path.exists():
|
||||
# class_map = YAML.load(config_path).get('class_map') or {}
|
||||
# if class_map:
|
||||
# return {str(key): int(value) for key, value in class_map.items()}
|
||||
return DEFAULT_CLASS_MAP.copy()
|
||||
|
||||
|
||||
def _stringify(value: float | int) -> str:
|
||||
"""Convert numeric values to the string form used by evaluator JSON files."""
|
||||
return str(value)
|
||||
|
||||
|
||||
def _denormalize_box(x_norm: float, y_norm: float, w_norm: float, h_norm: float, img_width: int, img_height: int) -> list[str]:
|
||||
"""Convert normalized xywh center box coordinates to absolute xyxy pixel strings."""
|
||||
x_center_px = x_norm * img_width
|
||||
y_center_px = y_norm * img_height
|
||||
w_px = w_norm * img_width
|
||||
h_px = h_norm * img_height
|
||||
return [
|
||||
_stringify(x_center_px - w_px / 2),
|
||||
_stringify(y_center_px - h_px / 2),
|
||||
_stringify(x_center_px + w_px / 2),
|
||||
_stringify(y_center_px + h_px / 2),
|
||||
]
|
||||
|
||||
|
||||
def _empty_3d_result(result: dict) -> dict:
|
||||
"""Populate a JSON entry with empty 3D fields."""
|
||||
result["3d_ori"] = EMPTY_3D_ORI.copy()
|
||||
return _empty_3d_faces(result)
|
||||
|
||||
|
||||
def _empty_3d_faces(result: dict) -> dict:
|
||||
"""Populate a JSON entry with empty face fields."""
|
||||
result["3d_front"] = EMPTY_3D_FACE.copy()
|
||||
result["3d_back"] = EMPTY_3D_FACE.copy()
|
||||
result["3d_left"] = EMPTY_3D_FACE.copy()
|
||||
result["3d_right"] = EMPTY_3D_FACE.copy()
|
||||
return result
|
||||
|
||||
|
||||
def _build_face(face_values: list[float], img_width: int, img_height: int) -> list[str]:
|
||||
"""Convert one 8-value face block to evaluator JSON format."""
|
||||
return [
|
||||
_stringify(face_values[0]),
|
||||
_stringify(face_values[1]),
|
||||
_stringify(face_values[2]),
|
||||
_stringify(face_values[3]),
|
||||
_stringify(face_values[4] * img_width),
|
||||
_stringify(face_values[5] * img_height),
|
||||
_stringify(face_values[6]),
|
||||
_stringify(face_values[7]),
|
||||
]
|
||||
|
||||
|
||||
def _extract_occlusion(parts: list[float], ncols: int) -> float:
|
||||
"""Extract the occlusion attribute from a parsed txt line."""
|
||||
if ncols in {6, 19, 51}:
|
||||
return int(parts[-1])
|
||||
if ncols == 7:
|
||||
return int(parts[-2])
|
||||
raise ValueError(f"Unsupported label column count {ncols} for occlusion extraction")
|
||||
|
||||
|
||||
def parse_txt_line(line, class_map, img_width=1920, img_height=1080):
|
||||
"""
|
||||
Parse a single line from the txt file and convert to JSON object structure.
|
||||
|
||||
Args:
|
||||
line: Single line from txt file
|
||||
img_width: Image width for denormalization
|
||||
img_height: Image height for denormalization
|
||||
|
||||
Returns:
|
||||
Dictionary with parsed data in JSON format
|
||||
"""
|
||||
|
||||
raw = line.strip().split()
|
||||
if len(raw) < 2:
|
||||
return None
|
||||
|
||||
label_name = raw[0]
|
||||
label = class_map.get(label_name)
|
||||
if label is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
parts = list(map(float, raw[1:]))
|
||||
except ValueError:
|
||||
return None
|
||||
ncols = len(raw)
|
||||
if len(parts) < 4:
|
||||
return None
|
||||
|
||||
x_norm, y_norm, w_norm, h_norm = parts[0:4]
|
||||
|
||||
result = {
|
||||
"type": str(label),
|
||||
"type_name": label_name,
|
||||
"roi_id": "1",
|
||||
"occlusion": _stringify(_extract_occlusion(parts, ncols)),
|
||||
"box2d": _denormalize_box(x_norm, y_norm, w_norm, h_norm, img_width, img_height),
|
||||
}
|
||||
|
||||
if ncols in {6, 7}:
|
||||
return _empty_3d_result(result)
|
||||
|
||||
if ncols == 19:
|
||||
x3d_ori, y3d_ori, z3d_ori = parts[4:7]
|
||||
l3d, h3d, w3d = parts[7:10]
|
||||
rot_y = parts[10]
|
||||
xc_ori, yc_ori = parts[11:13]
|
||||
xc_ori_d, yc_ori_d = parts[13:15]
|
||||
alpha_ori = parts[15]
|
||||
flag = parts[16]
|
||||
|
||||
result["3d_ori"] = [
|
||||
_stringify(x3d_ori),
|
||||
_stringify(y3d_ori),
|
||||
_stringify(z3d_ori),
|
||||
_stringify(l3d),
|
||||
_stringify(h3d),
|
||||
_stringify(w3d),
|
||||
_stringify(rot_y),
|
||||
_stringify(xc_ori * img_width),
|
||||
_stringify(yc_ori * img_height),
|
||||
_stringify(xc_ori_d * img_width),
|
||||
_stringify(yc_ori_d * img_height),
|
||||
_stringify(alpha_ori),
|
||||
_stringify(int(flag) if float(flag).is_integer() else flag),
|
||||
]
|
||||
return _empty_3d_faces(result)
|
||||
|
||||
if ncols == 51:
|
||||
x3d_ori, y3d_ori, z3d_ori = parts[4:7]
|
||||
l3d, h3d, w3d = parts[7:10]
|
||||
rot_y = parts[10]
|
||||
xc_ori, yc_ori = parts[11:13]
|
||||
xc_ori_d, yc_ori_d = parts[13:15]
|
||||
alpha_ori = parts[15]
|
||||
flag = parts[16]
|
||||
|
||||
result["3d_ori"] = [
|
||||
_stringify(x3d_ori),
|
||||
_stringify(y3d_ori),
|
||||
_stringify(z3d_ori),
|
||||
_stringify(l3d),
|
||||
_stringify(h3d),
|
||||
_stringify(w3d),
|
||||
_stringify(rot_y),
|
||||
_stringify(xc_ori * img_width),
|
||||
_stringify(yc_ori * img_height),
|
||||
_stringify(xc_ori_d * img_width),
|
||||
_stringify(yc_ori_d * img_height),
|
||||
_stringify(alpha_ori),
|
||||
_stringify(int(flag) if float(flag).is_integer() else flag),
|
||||
]
|
||||
result["3d_front"] = _build_face(parts[17:25], img_width, img_height)
|
||||
result["3d_back"] = _build_face(parts[25:33], img_width, img_height)
|
||||
result["3d_left"] = _build_face(parts[33:41], img_width, img_height)
|
||||
result["3d_right"] = _build_face(parts[41:49], img_width, img_height)
|
||||
return result
|
||||
|
||||
raise ValueError(f"Unsupported label column count {ncols} for line: {line}")
|
||||
|
||||
|
||||
def _resolve_convert_args(class_map_or_img_width, img_width, img_height, data_config_path):
|
||||
"""Support both legacy convert_txt_to_json(txt, json, w, h) and current class_map-based calls."""
|
||||
if isinstance(class_map_or_img_width, dict):
|
||||
return class_map_or_img_width, int(img_width), int(img_height)
|
||||
|
||||
if isinstance(class_map_or_img_width, (int, float)) and not isinstance(class_map_or_img_width, bool):
|
||||
return load_class_map(data_config_path), int(class_map_or_img_width), int(img_width)
|
||||
|
||||
if class_map_or_img_width is None:
|
||||
return load_class_map(data_config_path), int(img_width), int(img_height)
|
||||
|
||||
raise TypeError("class_map_or_img_width must be a class_map dict, image width, or None")
|
||||
|
||||
|
||||
def convert_txt_to_json(
|
||||
txt_file,
|
||||
json_file,
|
||||
class_map_or_img_width=None,
|
||||
img_width=1920,
|
||||
img_height=1080,
|
||||
data_config_path: str | Path | None = None,
|
||||
):
|
||||
"""
|
||||
Convert txt ground truth file to JSON format.
|
||||
|
||||
Args:
|
||||
txt_file: Path to input txt file
|
||||
json_file: Path to output JSON file
|
||||
img_width: Image width for denormalization
|
||||
img_height: Image height for denormalization
|
||||
"""
|
||||
txt_path = Path(txt_file)
|
||||
json_path = Path(json_file)
|
||||
class_map, img_width, img_height = _resolve_convert_args(
|
||||
class_map_or_img_width,
|
||||
img_width,
|
||||
img_height,
|
||||
data_config_path,
|
||||
)
|
||||
|
||||
if not txt_path.exists():
|
||||
raise FileNotFoundError(f"Input file not found: {txt_file}")
|
||||
|
||||
# Read txt file
|
||||
with open(txt_path, 'r') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
# Parse each line and build JSON structure
|
||||
json_data = {}
|
||||
for idx, line in enumerate(lines):
|
||||
line = line.strip()
|
||||
if not line: # Skip empty lines
|
||||
continue
|
||||
|
||||
obj_data = parse_txt_line(line, class_map, img_width, img_height)
|
||||
if obj_data:
|
||||
json_data[str(idx)] = obj_data
|
||||
|
||||
# Write JSON file
|
||||
with open(json_path, 'w') as f:
|
||||
json.dump(json_data, f, indent=4)
|
||||
|
||||
print(f"Converted {len(json_data)} objects from {txt_file} to {json_file}")
|
||||
print(f"Image dimensions used: {img_width}x{img_height}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Convert ground truth txt format to JSON format'
|
||||
)
|
||||
parser.add_argument('input_txt', help='Input txt file path')
|
||||
parser.add_argument('output_json', help='Output JSON file path')
|
||||
parser.add_argument('--image-width', type=int, default=1920,
|
||||
help='Image width for denormalization (default: 1920)')
|
||||
parser.add_argument('--image-height', type=int, default=1080,
|
||||
help='Image height for denormalization (default: 1080)')
|
||||
parser.add_argument(
|
||||
'--data-config',
|
||||
type=str,
|
||||
default='', #str(DEFAULT_DATA_CONFIG),
|
||||
help='Dataset YAML path used to load class_map (default: mono3d_ground.yaml)',
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
convert_txt_to_json(
|
||||
args.input_txt,
|
||||
args.output_json,
|
||||
class_map_or_img_width=None,
|
||||
img_width=args.image_width,
|
||||
img_height=args.image_height,
|
||||
data_config_path=args.data_config,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
793
tools/data_mining/mine_balanced_eval_subset.py
Executable file
793
tools/data_mining/mine_balanced_eval_subset.py
Executable file
@@ -0,0 +1,793 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import random
|
||||
import shutil
|
||||
import sys
|
||||
from collections import Counter, defaultdict
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from convert_txt_to_json import convert_txt_to_json
|
||||
|
||||
try:
|
||||
import yaml # type: ignore
|
||||
except ImportError: # pragma: no cover - optional dependency fallback
|
||||
yaml = None
|
||||
|
||||
try:
|
||||
from ruamel.yaml import YAML # type: ignore
|
||||
except ImportError: # pragma: no cover - optional dependency fallback
|
||||
YAML = None
|
||||
|
||||
from ultralytics.data.ground3d_augment import parse_ground_3d_label_file
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FrameRecord:
|
||||
"""Lightweight frame summary used for balanced subset mining."""
|
||||
|
||||
source_index: int
|
||||
list_root: str
|
||||
label_entry: str
|
||||
manifest_entry: str
|
||||
label_path: str
|
||||
image_path: str
|
||||
present_classes: tuple[int, ...]
|
||||
present_class_names: tuple[str, ...]
|
||||
instance_count_per_class: dict[int, int]
|
||||
num_objects: int
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Mine a class-balanced Ground3D evaluation subset from a train/val GT list."
|
||||
)
|
||||
parser.add_argument("--data", type=str, required=True, help="Dataset yaml, e.g. ultralytics/cfg/datasets/mono3d_ground.yaml")
|
||||
parser.add_argument("--split", type=str, default="val", help="Split key in data yaml, e.g. train or val.")
|
||||
parser.add_argument("--target-frames", type=int, default=300, help="Target number of frames to select.")
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
type=str,
|
||||
default="",
|
||||
help="Directory used for mined outputs. Defaults to <data-yaml-dir>/mined_subsets.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-prefix",
|
||||
type=str,
|
||||
default="",
|
||||
help="Optional filename prefix. Defaults to balanced_<split>_<selected_frames>.",
|
||||
)
|
||||
parser.add_argument("--seed", type=int, default=20260401, help="Random seed used for tie-breaking.")
|
||||
parser.add_argument(
|
||||
"--classes",
|
||||
type=int,
|
||||
nargs="*",
|
||||
default=None,
|
||||
help="Optional mapped class ids to balance. Defaults to all classes defined by class_map.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--include-empty",
|
||||
action="store_true",
|
||||
help="Allow frames without any selected classes into the candidate pool.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-entries",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Optional cap on scanned split entries for debugging. 0 scans the whole split.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sample-selection",
|
||||
choices=("head", "random"),
|
||||
default="head",
|
||||
help="How to subsample split entries when --max-entries is set.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-export-assets",
|
||||
action="store_true",
|
||||
help="Do not copy selected image and label files into the output directory.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-json-convert",
|
||||
action="store_true",
|
||||
help="Do not convert exported label txt files to JSON format.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--image-width",
|
||||
type=int,
|
||||
default=1920,
|
||||
help="Image width used for label coordinate denormalization when converting to JSON (default: 1920).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--image-height",
|
||||
type=int,
|
||||
default=1080,
|
||||
help="Image height used for label coordinate denormalization when converting to JSON (default: 1080).",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def load_yaml(path: str | Path) -> dict[str, Any]:
|
||||
path = Path(path)
|
||||
if yaml is not None:
|
||||
with path.open("r", encoding="utf-8") as file:
|
||||
return yaml.safe_load(file) or {}
|
||||
if YAML is not None:
|
||||
yaml_loader = YAML(typ="safe")
|
||||
with path.open("r", encoding="utf-8") as file:
|
||||
return yaml_loader.load(file) or {}
|
||||
raise RuntimeError("Neither PyYAML nor ruamel.yaml is available. Please install one YAML parser.")
|
||||
|
||||
|
||||
def resolve_dataset_root(data_yaml: Path, dataset_root: str | None) -> Path | None:
|
||||
if not dataset_root:
|
||||
return None
|
||||
root_path = Path(str(dataset_root))
|
||||
if root_path.is_absolute():
|
||||
return root_path.resolve()
|
||||
candidates = [(data_yaml.parent / root_path).resolve(), root_path.resolve()]
|
||||
for candidate in candidates:
|
||||
if candidate.exists():
|
||||
return candidate
|
||||
return candidates[0]
|
||||
|
||||
|
||||
def resolve_data_paths(data_yaml: Path, dataset_root: Path | None, value: Any) -> list[Path]:
|
||||
if value is None:
|
||||
raise ValueError(f"Missing split path in {data_yaml}")
|
||||
if isinstance(value, (list, tuple)):
|
||||
paths: list[Path] = []
|
||||
for item in value:
|
||||
paths.extend(resolve_data_paths(data_yaml, dataset_root, item))
|
||||
return paths
|
||||
|
||||
path = Path(str(value))
|
||||
if path.is_absolute():
|
||||
return [path.resolve()]
|
||||
|
||||
candidates = []
|
||||
if dataset_root is not None:
|
||||
candidates.append((dataset_root / path).resolve())
|
||||
candidates.append((data_yaml.parent / path).resolve())
|
||||
for candidate in candidates:
|
||||
if candidate.exists():
|
||||
return [candidate]
|
||||
return [candidates[0] if candidates else path.resolve()]
|
||||
|
||||
|
||||
def load_split_entries(
|
||||
split_files: list[Path],
|
||||
max_entries: int = 0,
|
||||
sample_selection: str = "head",
|
||||
sample_seed: int = 20260401,
|
||||
) -> list[tuple[str, str]]:
|
||||
entries: list[tuple[str, str]] = []
|
||||
for split_file in split_files:
|
||||
with split_file.open("r", encoding="utf-8") as file:
|
||||
for line in file:
|
||||
entry = line.strip()
|
||||
if not entry or entry.lstrip().startswith("#"):
|
||||
continue
|
||||
if Path(entry).suffix.lower() != ".txt":
|
||||
raise ValueError(f"Ground3D split entries must point to label .txt files, but got: {entry}")
|
||||
entries.append((str(split_file.parent.resolve()), entry))
|
||||
|
||||
if not entries:
|
||||
raise FileNotFoundError(f"No usable entries found in split files: {', '.join(str(p) for p in split_files)}")
|
||||
|
||||
if max_entries > 0 and len(entries) > max_entries:
|
||||
if sample_selection == "random":
|
||||
rng = random.Random(int(sample_seed))
|
||||
indices = sorted(rng.sample(range(len(entries)), int(max_entries)))
|
||||
entries = [entries[idx] for idx in indices]
|
||||
else:
|
||||
entries = entries[:max_entries]
|
||||
return entries
|
||||
|
||||
|
||||
def label_rel_to_image_rel(label_path: Path) -> Path:
|
||||
parts = list(label_path.parts)
|
||||
if "labels" in parts:
|
||||
parts[len(parts) - 1 - parts[::-1].index("labels")] = "images"
|
||||
return Path(*parts).with_suffix(".png")
|
||||
|
||||
|
||||
def entry_to_label_file(entry: tuple[str, str]) -> Path:
|
||||
list_root, label_entry = entry
|
||||
path = Path(label_entry)
|
||||
return path.resolve() if path.is_absolute() else (Path(list_root) / path).resolve()
|
||||
|
||||
|
||||
def entry_to_image_file(entry: tuple[str, str], image_root: Path | None) -> Path:
|
||||
_, label_entry = entry
|
||||
label_path = Path(label_entry)
|
||||
rel_image = label_rel_to_image_rel(label_path)
|
||||
if rel_image.is_absolute():
|
||||
return rel_image.resolve()
|
||||
if image_root is None:
|
||||
raise ValueError("Dataset image root is required to resolve relative Ground3D image paths.")
|
||||
return (image_root / rel_image).resolve()
|
||||
|
||||
|
||||
def infer_class_name_map(class_map: dict[str, int]) -> dict[int, str]:
|
||||
names: dict[int, str] = {}
|
||||
for raw_name, class_id in class_map.items():
|
||||
names.setdefault(int(class_id), str(raw_name))
|
||||
return {class_id: names[class_id] for class_id in sorted(names)}
|
||||
|
||||
|
||||
def build_frame_records(
|
||||
entries: list[tuple[str, str]],
|
||||
image_root: Path | None,
|
||||
class_map: dict[str, int],
|
||||
difficulty_weights: list[float],
|
||||
face_3d_classes: set[int],
|
||||
complete_3d_classes: set[int],
|
||||
class_name_map: dict[int, str],
|
||||
requested_classes: set[int],
|
||||
include_empty: bool,
|
||||
) -> tuple[list[FrameRecord], dict[str, int]]:
|
||||
records: list[FrameRecord] = []
|
||||
stats = {
|
||||
"total_entries": len(entries),
|
||||
"missing_labels": 0,
|
||||
"missing_images": 0,
|
||||
"empty_frames": 0,
|
||||
"kept_frames": 0,
|
||||
}
|
||||
|
||||
for source_index, entry in enumerate(entries):
|
||||
label_path = entry_to_label_file(entry)
|
||||
image_path = entry_to_image_file(entry, image_root)
|
||||
|
||||
if not label_path.is_file():
|
||||
stats["missing_labels"] += 1
|
||||
continue
|
||||
if not image_path.is_file():
|
||||
stats["missing_images"] += 1
|
||||
continue
|
||||
|
||||
lb_2d, _ = parse_ground_3d_label_file(
|
||||
str(label_path),
|
||||
class_map,
|
||||
difficulty_weights,
|
||||
face_3d_classes,
|
||||
complete_3d_classes,
|
||||
)
|
||||
|
||||
raw_classes = [int(class_id) for class_id in lb_2d["cls"].reshape(-1).tolist()]
|
||||
instance_counts = Counter(class_id for class_id in raw_classes if class_id in requested_classes)
|
||||
present_classes = tuple(sorted(instance_counts))
|
||||
|
||||
if not present_classes and not include_empty:
|
||||
stats["empty_frames"] += 1
|
||||
continue
|
||||
|
||||
present_class_names = tuple(class_name_map.get(class_id, str(class_id)) for class_id in present_classes)
|
||||
records.append(
|
||||
FrameRecord(
|
||||
source_index=source_index,
|
||||
list_root=entry[0],
|
||||
label_entry=entry[1],
|
||||
manifest_entry=str(label_path),
|
||||
label_path=str(label_path),
|
||||
image_path=str(image_path),
|
||||
present_classes=present_classes,
|
||||
present_class_names=present_class_names,
|
||||
instance_count_per_class=dict(instance_counts),
|
||||
num_objects=int(sum(instance_counts.values())),
|
||||
)
|
||||
)
|
||||
|
||||
stats["kept_frames"] = len(records)
|
||||
return records, stats
|
||||
|
||||
|
||||
def summarize_records(records: list[FrameRecord]) -> tuple[Counter[int], Counter[int]]:
|
||||
frame_counts: Counter[int] = Counter()
|
||||
instance_counts: Counter[int] = Counter()
|
||||
for record in records:
|
||||
for class_id in record.present_classes:
|
||||
frame_counts[class_id] += 1
|
||||
instance_counts[class_id] += int(record.instance_count_per_class.get(class_id, 0))
|
||||
return frame_counts, instance_counts
|
||||
|
||||
|
||||
def build_candidate_rankings(
|
||||
records: list[FrameRecord],
|
||||
active_classes: list[int],
|
||||
available_frame_counts: Counter[int],
|
||||
seed: int,
|
||||
) -> tuple[dict[int, list[int]], list[int]]:
|
||||
rng = random.Random(seed)
|
||||
shuffled_indices = list(range(len(records)))
|
||||
rng.shuffle(shuffled_indices)
|
||||
tie_rank = {index: rank for rank, index in enumerate(shuffled_indices)}
|
||||
|
||||
rarity_score: dict[int, float] = {}
|
||||
active_span: dict[int, int] = {}
|
||||
overall_score: dict[int, tuple[float, int, int, int]] = {}
|
||||
candidates_by_class: dict[int, list[int]] = defaultdict(list)
|
||||
|
||||
active_class_set = set(active_classes)
|
||||
for index, record in enumerate(records):
|
||||
covered_classes = [class_id for class_id in record.present_classes if class_id in active_class_set]
|
||||
active_span[index] = len(covered_classes)
|
||||
rarity_score[index] = sum(1.0 / max(int(available_frame_counts[class_id]), 1) for class_id in covered_classes)
|
||||
overall_score[index] = (
|
||||
rarity_score[index],
|
||||
active_span[index],
|
||||
record.num_objects,
|
||||
-tie_rank[index],
|
||||
)
|
||||
for class_id in covered_classes:
|
||||
candidates_by_class[class_id].append(index)
|
||||
|
||||
for class_id, indices in candidates_by_class.items():
|
||||
indices.sort(
|
||||
key=lambda index: (
|
||||
-overall_score[index][0],
|
||||
-overall_score[index][1],
|
||||
-overall_score[index][2],
|
||||
tie_rank[index],
|
||||
)
|
||||
)
|
||||
|
||||
fill_order = list(range(len(records)))
|
||||
fill_order.sort(
|
||||
key=lambda index: (
|
||||
-overall_score[index][0],
|
||||
-overall_score[index][1],
|
||||
-overall_score[index][2],
|
||||
tie_rank[index],
|
||||
)
|
||||
)
|
||||
return candidates_by_class, fill_order
|
||||
|
||||
|
||||
def select_balanced_subset(
|
||||
records: list[FrameRecord],
|
||||
target_frames: int,
|
||||
active_classes: list[int],
|
||||
seed: int,
|
||||
) -> tuple[list[FrameRecord], Counter[int]]:
|
||||
if target_frames <= 0 or not records:
|
||||
return [], Counter()
|
||||
|
||||
if target_frames >= len(records):
|
||||
selected_frame_counts, _ = summarize_records(records)
|
||||
return list(records), selected_frame_counts
|
||||
|
||||
available_frame_counts, _ = summarize_records(records)
|
||||
candidates_by_class, fill_order = build_candidate_rankings(records, active_classes, available_frame_counts, seed)
|
||||
selected_indices: list[int] = []
|
||||
selected_set: set[int] = set()
|
||||
selected_frame_counts: Counter[int] = Counter()
|
||||
next_ptr = {class_id: 0 for class_id in active_classes}
|
||||
exhausted_classes: set[int] = set()
|
||||
|
||||
while len(selected_indices) < target_frames and len(exhausted_classes) < len(active_classes):
|
||||
focus_class = min(
|
||||
(class_id for class_id in active_classes if class_id not in exhausted_classes),
|
||||
key=lambda class_id: (
|
||||
selected_frame_counts[class_id],
|
||||
available_frame_counts[class_id],
|
||||
class_id,
|
||||
),
|
||||
)
|
||||
|
||||
candidate_list = candidates_by_class.get(focus_class, [])
|
||||
pointer = next_ptr[focus_class]
|
||||
while pointer < len(candidate_list) and candidate_list[pointer] in selected_set:
|
||||
pointer += 1
|
||||
next_ptr[focus_class] = pointer
|
||||
|
||||
if pointer >= len(candidate_list):
|
||||
exhausted_classes.add(focus_class)
|
||||
continue
|
||||
|
||||
chosen_index = candidate_list[pointer]
|
||||
next_ptr[focus_class] = pointer + 1
|
||||
selected_indices.append(chosen_index)
|
||||
selected_set.add(chosen_index)
|
||||
for class_id in records[chosen_index].present_classes:
|
||||
selected_frame_counts[class_id] += 1
|
||||
|
||||
if len(selected_indices) < target_frames:
|
||||
for index in fill_order:
|
||||
if index in selected_set:
|
||||
continue
|
||||
selected_indices.append(index)
|
||||
selected_set.add(index)
|
||||
for class_id in records[index].present_classes:
|
||||
selected_frame_counts[class_id] += 1
|
||||
if len(selected_indices) >= target_frames:
|
||||
break
|
||||
|
||||
selected_records = [records[index] for index in selected_indices]
|
||||
return selected_records, selected_frame_counts
|
||||
|
||||
|
||||
def write_manifest(path: Path, records: list[FrameRecord], meta: dict[str, Any]) -> None:
|
||||
with path.open("w", encoding="utf-8") as file:
|
||||
file.write(f"# generated_at: {meta['generated_at']}\n")
|
||||
file.write(f"# data_yaml: {meta['data_yaml']}\n")
|
||||
file.write(f"# split: {meta['split']}\n")
|
||||
file.write(f"# target_frames: {meta['target_frames']}\n")
|
||||
file.write(f"# selected_frames: {meta['selected_frames']}\n")
|
||||
manifest_entries = meta.get("manifest_entries")
|
||||
if manifest_entries is None:
|
||||
manifest_entries = [record.manifest_entry for record in records]
|
||||
for manifest_entry in manifest_entries:
|
||||
file.write(f"{manifest_entry}\n")
|
||||
|
||||
|
||||
def infer_export_rel_label_path(record: FrameRecord) -> Path:
|
||||
label_entry_path = Path(record.label_entry)
|
||||
if not label_entry_path.is_absolute():
|
||||
return label_entry_path
|
||||
|
||||
label_path = Path(record.label_path)
|
||||
parts = list(label_path.parts)
|
||||
if "labels" in parts:
|
||||
label_idx = len(parts) - 1 - parts[::-1].index("labels")
|
||||
return Path(*parts[label_idx:])
|
||||
return Path(label_path.name)
|
||||
|
||||
|
||||
def normalize_export_subpath(path: Path, anchor: str) -> Path:
|
||||
if path.parts and path.parts[0] == anchor:
|
||||
return path.relative_to(anchor)
|
||||
return path
|
||||
|
||||
|
||||
def infer_calib_dir_tasks(image_path: Path, rel_image_path: Path, image_root: Path | None, calib_root: Path) -> list[tuple[Path, Path]]:
|
||||
"""Return (src_calib_dir, dst_calib_dir) pairs for the case that owns *image_path*.
|
||||
|
||||
The source data layout has a ``calib/`` folder at the case level, i.e. the
|
||||
directory that also contains the ``images/`` sub-folder. We locate that
|
||||
folder by walking up the resolved image path until we find a sibling
|
||||
``calib/`` directory. The destination mirrors the same relative structure
|
||||
under *calib_root*.
|
||||
"""
|
||||
tasks: list[tuple[Path, Path]] = []
|
||||
|
||||
# Walk up the image path to find the case directory that has a sibling calib/ folder.
|
||||
resolved = image_path.resolve()
|
||||
candidate_dir = resolved.parent
|
||||
while candidate_dir != candidate_dir.parent:
|
||||
src_calib_dir = candidate_dir / "calib"
|
||||
if src_calib_dir.is_dir():
|
||||
# Determine the relative destination path.
|
||||
# Prefer a path relative to image_root when possible.
|
||||
if image_root is not None:
|
||||
try:
|
||||
rel_case = candidate_dir.relative_to(image_root.resolve())
|
||||
dst_calib_dir = calib_root / rel_case / "calib"
|
||||
tasks.append((src_calib_dir, dst_calib_dir))
|
||||
return tasks
|
||||
except ValueError:
|
||||
pass
|
||||
# Fallback: use the rel_image_path to infer the case directory depth.
|
||||
rel_parts = list(rel_image_path.parts)
|
||||
if "images" in rel_parts:
|
||||
case_parts = rel_parts[: len(rel_parts) - 1 - rel_parts[::-1].index("images")]
|
||||
dst_calib_dir = calib_root / Path(*case_parts) / "calib" if case_parts else calib_root / "calib"
|
||||
else:
|
||||
dst_calib_dir = calib_root / candidate_dir.name / "calib"
|
||||
tasks.append((src_calib_dir, dst_calib_dir))
|
||||
return tasks
|
||||
candidate_dir = candidate_dir.parent
|
||||
|
||||
return tasks
|
||||
|
||||
|
||||
def export_selected_assets(
|
||||
output_dir: Path,
|
||||
records: list[FrameRecord],
|
||||
image_root: Path | None,
|
||||
skip_json_convert: bool = False,
|
||||
image_width: int = 1920,
|
||||
image_height: int = 1080,
|
||||
) -> dict[str, Any]:
|
||||
# Output layout: output_dir/<case_path>/labels|images|calib|labels_json/...
|
||||
exported_manifest_entries: list[str] = []
|
||||
copied_calib_targets: set[Path] = set()
|
||||
copied_calib_count = 0
|
||||
converted_json_count = 0
|
||||
|
||||
for record in records:
|
||||
rel_label_path = infer_export_rel_label_path(record)
|
||||
rel_image_path = label_rel_to_image_rel(rel_label_path)
|
||||
dst_label_path = output_dir / normalize_export_subpath(rel_label_path, "labels")
|
||||
dst_image_path = output_dir / normalize_export_subpath(rel_image_path, "images")
|
||||
|
||||
dst_label_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
dst_image_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(record.label_path, dst_label_path)
|
||||
shutil.copy2(record.image_path, dst_image_path)
|
||||
|
||||
if not skip_json_convert:
|
||||
rel_parts = list(normalize_export_subpath(rel_label_path, "labels").parts)
|
||||
if "labels" in rel_parts:
|
||||
idx = len(rel_parts) - 1 - rel_parts[::-1].index("labels")
|
||||
rel_parts[idx] = "labels_json"
|
||||
dst_json_path = (output_dir / Path(*rel_parts)).with_suffix(".json")
|
||||
dst_json_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
convert_txt_to_json(str(dst_label_path), str(dst_json_path), image_width, image_height)
|
||||
converted_json_count += 1
|
||||
|
||||
calib_tasks = infer_calib_dir_tasks(Path(record.image_path), rel_image_path, image_root, output_dir)
|
||||
for src_calib_dir, dst_calib_dir in calib_tasks:
|
||||
if dst_calib_dir in copied_calib_targets:
|
||||
continue
|
||||
copied_calib_targets.add(dst_calib_dir)
|
||||
if dst_calib_dir.exists():
|
||||
# Already copied by a previous frame from the same case.
|
||||
continue
|
||||
dst_calib_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copytree(src_calib_dir, dst_calib_dir)
|
||||
copied_calib_count += 1
|
||||
|
||||
exported_manifest_entries.append(str(dst_label_path.relative_to(output_dir)))
|
||||
|
||||
return {
|
||||
"output_root": str(output_dir),
|
||||
"labels_root": str(output_dir),
|
||||
"images_root": str(output_dir),
|
||||
"calib_root": str(output_dir),
|
||||
"labels_json_root": str(output_dir) if not skip_json_convert else None,
|
||||
"calib_files": copied_calib_count,
|
||||
"json_files": converted_json_count,
|
||||
"manifest_entries": exported_manifest_entries,
|
||||
}
|
||||
|
||||
|
||||
def write_csv(path: Path, records: list[FrameRecord]) -> None:
|
||||
with path.open("w", encoding="utf-8", newline="") as file:
|
||||
writer = csv.DictWriter(
|
||||
file,
|
||||
fieldnames=[
|
||||
"selected_rank",
|
||||
"source_index",
|
||||
"label_entry",
|
||||
"label_path",
|
||||
"image_path",
|
||||
"num_objects",
|
||||
"present_class_ids",
|
||||
"present_class_names",
|
||||
"instance_count_per_class",
|
||||
],
|
||||
)
|
||||
writer.writeheader()
|
||||
for rank, record in enumerate(records, start=1):
|
||||
writer.writerow(
|
||||
{
|
||||
"selected_rank": rank,
|
||||
"source_index": record.source_index,
|
||||
"label_entry": record.label_entry,
|
||||
"label_path": record.label_path,
|
||||
"image_path": record.image_path,
|
||||
"num_objects": record.num_objects,
|
||||
"present_class_ids": " ".join(str(class_id) for class_id in record.present_classes),
|
||||
"present_class_names": " ".join(record.present_class_names),
|
||||
"instance_count_per_class": json.dumps(record.instance_count_per_class, ensure_ascii=False, sort_keys=True),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def build_stats_payload(
|
||||
args: argparse.Namespace,
|
||||
data_yaml: Path,
|
||||
split_files: list[Path],
|
||||
image_root: Path | None,
|
||||
class_name_map: dict[int, str],
|
||||
requested_classes: list[int],
|
||||
scan_stats: dict[str, int],
|
||||
candidate_records: list[FrameRecord],
|
||||
selected_records: list[FrameRecord],
|
||||
manifest_path: Path,
|
||||
csv_path: Path,
|
||||
stats_path: Path,
|
||||
exported_assets: dict[str, Any] | None,
|
||||
) -> dict[str, Any]:
|
||||
candidate_frame_counts, candidate_instance_counts = summarize_records(candidate_records)
|
||||
selected_frame_counts, selected_instance_counts = summarize_records(selected_records)
|
||||
active_classes = [class_id for class_id in requested_classes if candidate_frame_counts[class_id] > 0]
|
||||
|
||||
return {
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"data_yaml": str(data_yaml),
|
||||
"split": str(args.split),
|
||||
"split_files": [str(path) for path in split_files],
|
||||
"image_root": str(image_root) if image_root is not None else None,
|
||||
"target_frames": int(args.target_frames),
|
||||
"selected_frames": len(selected_records),
|
||||
"candidate_frames": len(candidate_records),
|
||||
"seed": int(args.seed),
|
||||
"include_empty": bool(args.include_empty),
|
||||
"requested_classes": requested_classes,
|
||||
"active_classes": active_classes,
|
||||
"scan_stats": scan_stats,
|
||||
"outputs": {
|
||||
"manifest": str(manifest_path),
|
||||
"frame_csv": str(csv_path),
|
||||
"stats_json": str(stats_path),
|
||||
"images_dir": exported_assets.get("images_root") if exported_assets else None,
|
||||
"labels_dir": exported_assets.get("labels_root") if exported_assets else None,
|
||||
"labels_json_dir": exported_assets.get("labels_json_root") if exported_assets else None,
|
||||
"calib_dir": exported_assets.get("calib_root") if exported_assets else None,
|
||||
"calib_files": exported_assets.get("calib_files") if exported_assets else 0,
|
||||
"json_files": exported_assets.get("json_files") if exported_assets else 0,
|
||||
},
|
||||
"per_class": {
|
||||
str(class_id): {
|
||||
"name": class_name_map.get(class_id, str(class_id)),
|
||||
"candidate_frame_count": int(candidate_frame_counts[class_id]),
|
||||
"selected_frame_count": int(selected_frame_counts[class_id]),
|
||||
"candidate_instance_count": int(candidate_instance_counts[class_id]),
|
||||
"selected_instance_count": int(selected_instance_counts[class_id]),
|
||||
}
|
||||
for class_id in requested_classes
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def print_summary(
|
||||
requested_classes: list[int],
|
||||
class_name_map: dict[int, str],
|
||||
candidate_records: list[FrameRecord],
|
||||
selected_records: list[FrameRecord],
|
||||
stats_payload: dict[str, Any],
|
||||
) -> None:
|
||||
candidate_frame_counts, _ = summarize_records(candidate_records)
|
||||
selected_frame_counts, _ = summarize_records(selected_records)
|
||||
print(f"Scanned candidate frames: {len(candidate_records)}")
|
||||
print(f"Selected frames: {len(selected_records)} / target {stats_payload['target_frames']}")
|
||||
print(
|
||||
"Scan stats: "
|
||||
f"total_entries={stats_payload['scan_stats']['total_entries']} "
|
||||
f"missing_labels={stats_payload['scan_stats']['missing_labels']} "
|
||||
f"missing_images={stats_payload['scan_stats']['missing_images']} "
|
||||
f"empty_frames={stats_payload['scan_stats']['empty_frames']}"
|
||||
)
|
||||
print("Per-class selected frame counts:")
|
||||
for class_id in requested_classes:
|
||||
name = class_name_map.get(class_id, str(class_id))
|
||||
print(
|
||||
f" class {class_id:>2} ({name:<20}) "
|
||||
f"candidate={candidate_frame_counts[class_id]:>5} selected={selected_frame_counts[class_id]:>4}"
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
data_yaml = Path(args.data).resolve()
|
||||
data_cfg = load_yaml(data_yaml)
|
||||
|
||||
dataset_root = resolve_dataset_root(data_yaml, data_cfg.get("path"))
|
||||
split_files = resolve_data_paths(data_yaml, dataset_root, data_cfg.get(args.split))
|
||||
image_root = dataset_root
|
||||
|
||||
class_map = {str(key): int(value) for key, value in (data_cfg.get("class_map") or {}).items()}
|
||||
if not class_map:
|
||||
raise ValueError(f"`class_map` is required in {data_yaml} for Ground3D subset mining.")
|
||||
class_name_map = infer_class_name_map(class_map)
|
||||
all_class_ids = sorted(set(class_name_map))
|
||||
requested_classes = sorted(set(int(class_id) for class_id in (args.classes if args.classes is not None else all_class_ids)))
|
||||
unknown_classes = [class_id for class_id in requested_classes if class_id not in class_name_map]
|
||||
if unknown_classes:
|
||||
raise ValueError(f"Requested classes are not present in class_map: {unknown_classes}")
|
||||
|
||||
difficulty_weights = [float(value) for value in data_cfg.get("difficulty_weights", [1.0, 1.0, 1.0, 1.0])]
|
||||
face_3d_classes = set(int(value) for value in data_cfg.get("face_3d_classes", []))
|
||||
complete_3d_classes = set(int(value) for value in data_cfg.get("complete_3d_classes", []))
|
||||
|
||||
entries = load_split_entries(
|
||||
split_files=split_files,
|
||||
max_entries=int(args.max_entries),
|
||||
sample_selection=str(args.sample_selection),
|
||||
sample_seed=int(args.seed),
|
||||
)
|
||||
candidate_records, scan_stats = build_frame_records(
|
||||
entries=entries,
|
||||
image_root=image_root,
|
||||
class_map=class_map,
|
||||
difficulty_weights=difficulty_weights,
|
||||
face_3d_classes=face_3d_classes,
|
||||
complete_3d_classes=complete_3d_classes,
|
||||
class_name_map=class_name_map,
|
||||
requested_classes=set(requested_classes),
|
||||
include_empty=bool(args.include_empty),
|
||||
)
|
||||
if not candidate_records:
|
||||
raise RuntimeError("No candidate frames remain after filtering missing assets and empty labels.")
|
||||
|
||||
active_classes = [class_id for class_id in requested_classes if any(class_id in record.present_classes for record in candidate_records)]
|
||||
if not active_classes and not args.include_empty:
|
||||
raise RuntimeError("No requested classes were found in the candidate split.")
|
||||
|
||||
selected_records, _ = select_balanced_subset(
|
||||
records=candidate_records,
|
||||
target_frames=min(int(args.target_frames), len(candidate_records)),
|
||||
active_classes=active_classes,
|
||||
seed=int(args.seed),
|
||||
)
|
||||
|
||||
output_dir = Path(args.output_dir).resolve() if args.output_dir else (data_yaml.parent / "mined_subsets").resolve()
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
output_prefix = args.output_prefix or f"balanced_{args.split}_{len(selected_records)}"
|
||||
manifest_path = output_dir / f"{output_prefix}.txt"
|
||||
csv_path = output_dir / f"{output_prefix}_frames.csv"
|
||||
stats_path = output_dir / f"{output_prefix}_stats.json"
|
||||
exported_assets = None
|
||||
if not args.skip_export_assets:
|
||||
exported_assets = export_selected_assets(
|
||||
output_dir,
|
||||
selected_records,
|
||||
image_root,
|
||||
skip_json_convert=bool(args.skip_json_convert),
|
||||
image_width=int(args.image_width),
|
||||
image_height=int(args.image_height),
|
||||
)
|
||||
|
||||
stats_payload = build_stats_payload(
|
||||
args=args,
|
||||
data_yaml=data_yaml,
|
||||
split_files=split_files,
|
||||
image_root=image_root,
|
||||
class_name_map=class_name_map,
|
||||
requested_classes=requested_classes,
|
||||
scan_stats=scan_stats,
|
||||
candidate_records=candidate_records,
|
||||
selected_records=selected_records,
|
||||
manifest_path=manifest_path,
|
||||
csv_path=csv_path,
|
||||
stats_path=stats_path,
|
||||
exported_assets=exported_assets,
|
||||
)
|
||||
write_manifest(
|
||||
manifest_path,
|
||||
selected_records,
|
||||
meta={
|
||||
"generated_at": stats_payload["generated_at"],
|
||||
"data_yaml": str(data_yaml),
|
||||
"split": args.split,
|
||||
"target_frames": int(args.target_frames),
|
||||
"selected_frames": len(selected_records),
|
||||
"manifest_entries": exported_assets.get("manifest_entries") if exported_assets else None,
|
||||
},
|
||||
)
|
||||
write_csv(csv_path, selected_records)
|
||||
with stats_path.open("w", encoding="utf-8") as file:
|
||||
json.dump(stats_payload, file, ensure_ascii=False, indent=2, sort_keys=False)
|
||||
file.write("\n")
|
||||
|
||||
print_summary(
|
||||
requested_classes=requested_classes,
|
||||
class_name_map=class_name_map,
|
||||
candidate_records=candidate_records,
|
||||
selected_records=selected_records,
|
||||
stats_payload=stats_payload,
|
||||
)
|
||||
print(f"Manifest written to: {manifest_path}")
|
||||
print(f"Frame csv written to: {csv_path}")
|
||||
print(f"Stats written to: {stats_path}")
|
||||
if exported_assets:
|
||||
output_root = exported_assets["output_root"]
|
||||
print(f"Assets exported to: {output_root}")
|
||||
print(f" images/labels/calib/labels_json nested under each case directory")
|
||||
if exported_assets.get("labels_json_root"):
|
||||
print(f" Labels JSON converted: {exported_assets['json_files']} files")
|
||||
print(f" Calib dirs copied: {exported_assets['calib_files']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
8
tools/data_mining/mine_evalset.sh
Executable file
8
tools/data_mining/mine_evalset.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
/deeplearning_team/ydong/dongying/miniconda/envs/dev/bin/python \
|
||||
tools/data_mining/mine_balanced_eval_subset.py \
|
||||
--data ultralytics/cfg/datasets/mono3d_ground.yaml \
|
||||
--split val \
|
||||
--target-frames 2000 \
|
||||
--output-dir /data1/dongying/Mono3d/G1M3/data_for_alignment/mono3d_mining_val2000_20260410 \
|
||||
--output-prefix balanced_val_2000
|
||||
|
||||
3
tools/feishu_project/.gitignore
vendored
Executable file
3
tools/feishu_project/.gitignore
vendored
Executable file
@@ -0,0 +1,3 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
reports/
|
||||
160
tools/feishu_project/SKILL_fp.md
Executable file
160
tools/feishu_project/SKILL_fp.md
Executable file
@@ -0,0 +1,160 @@
|
||||
---
|
||||
name: feishu-project-issue-data
|
||||
description: Use when working in the yolo26-3d repository and the user wants to read Feishu Project issue views, export a view to structured JSON, classify issue data addresses, download issue data, repair affected standard-path downloads, validate case completeness, or run batch inference on downloaded issue data. Covers fp CLI plus tools/feishu_project/export_feishu_view_issues.py, download_issue_data.py/sh, and run_issue_data_inference.py/sh, including the 董颖-G1Q3 workflow.
|
||||
---
|
||||
|
||||
# Feishu Project Issue Data
|
||||
|
||||
Use the repo dev env for Python commands:
|
||||
|
||||
```bash
|
||||
/root/.codex/skills/use-dongying-dev-env/scripts/with-dev-env.sh python ...
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```bash
|
||||
/deeplearning_team/ydong/dongying/miniconda/envs/dev/bin/python ...
|
||||
```
|
||||
|
||||
Current repo defaults are documented in [../../feishu_project.md](../../feishu_project.md).
|
||||
|
||||
## When to use
|
||||
|
||||
- Read or verify a Feishu Project issue view with `fp`
|
||||
- Export a view such as `董颖-G1Q3` to structured JSON
|
||||
- Work with `问题数据地址` / `问题数据地址_PDCL`
|
||||
- Download issue data into the local workspace
|
||||
- Repair historical bad copies that only copied `sigmastar.1`
|
||||
- Check whether downloaded cases are inference-ready
|
||||
- Batch-run exported-model inference on downloaded issue data
|
||||
|
||||
## Core workflow
|
||||
|
||||
### 1. Verify Feishu access and view contents
|
||||
|
||||
Use `fp` directly when the user wants current data.
|
||||
|
||||
```bash
|
||||
fp view list -p <project_key> -u <user_key> -t issue --name "<view_name>"
|
||||
fp workitem list -o json -p <project_key> -u <user_key> --view "<view_name>" --all
|
||||
fp workitem get <issue_id> -o json -p <project_key> -u <user_key> -t issue
|
||||
```
|
||||
|
||||
### 2. Export structured issue JSON
|
||||
|
||||
Use [../../export_feishu_view_issues.py](../../export_feishu_view_issues.py).
|
||||
|
||||
```bash
|
||||
python ../../export_feishu_view_issues.py \
|
||||
--project-key <project_key> \
|
||||
--user-key <user_key> \
|
||||
--view-name "<view_name>" \
|
||||
--output ../../dongying_g1q3_issue_list.json
|
||||
```
|
||||
|
||||
The export should include:
|
||||
|
||||
- `缺陷标签池`
|
||||
- `问题数据地址`
|
||||
- `问题数据地址_PDCL`
|
||||
- `问题发生frameid`
|
||||
|
||||
### 3. Interpret data-address fields
|
||||
|
||||
Treat these as the same download class:
|
||||
|
||||
- pure `ADAS_...::...` clip references
|
||||
- `mdi raw -r ...` commands
|
||||
|
||||
Use this rule:
|
||||
|
||||
- `ADAS_xxx::yyy` is equivalent to `mdi raw -r ADAS_xxx::yyy -s .`
|
||||
|
||||
Standard paths use these normalization rules:
|
||||
|
||||
- rewrite `hfs/project-G1M3` or `project-G1M3` to `G1M3` when needed
|
||||
- if the path ends with `sigmastar.1`, copy the parent case dir
|
||||
- if the path ends with `sigmastar.1/camera4.bin`, copy the case dir above it
|
||||
- if the copied case has no local `test_data/calibs/camera4.json`, sync a shared parent `test_data` directory when present
|
||||
|
||||
### 4. Download or repair issue data
|
||||
|
||||
Use [../../download_issue_data.sh](../../download_issue_data.sh) or [../../download_issue_data.py](../../download_issue_data.py).
|
||||
|
||||
Common modes:
|
||||
|
||||
```bash
|
||||
DRY_RUN=1 bash ../../download_issue_data.sh
|
||||
```
|
||||
|
||||
```bash
|
||||
ONLY_REDOWNLOAD_AFFECTED_CASES=1 bash ../../download_issue_data.sh
|
||||
```
|
||||
|
||||
```bash
|
||||
SKIP_MDI=1 bash ../../download_issue_data.sh --issue-id <id>
|
||||
```
|
||||
|
||||
Defaults:
|
||||
|
||||
- download root: `/data1/dongying/Mono3d/G1Q3/feishu_project/downloaded_issue_data`
|
||||
- manifest: `<download_root>/download_manifest.json`
|
||||
|
||||
Use `ONLY_REDOWNLOAD_AFFECTED_CASES=1` to repair old standard-path copies that previously kept only `sigmastar.1`.
|
||||
|
||||
### 5. Validate inference readiness
|
||||
|
||||
Use the same case-resolution rules as [../../../model_inference/adapters/video_dir_inference_utils.py](../../../model_inference/adapters/video_dir_inference_utils.py).
|
||||
|
||||
A valid case must resolve:
|
||||
|
||||
- `*/sigmastar.1/camera4.bin`
|
||||
- a reachable `camera4.json` from one of:
|
||||
- `case_dir/test_data/calibs/camera4.json`
|
||||
- `case_dir.parent/test_data/calibs/camera4.json`
|
||||
- `case_dir/sigmastar.1/calibs/camera4.json`
|
||||
- `case_dir/calibs/camera4.json`
|
||||
|
||||
Prefer validating with the actual inference path-resolution logic instead of ad hoc file checks.
|
||||
|
||||
### 6. Run batch inference on downloaded issue data
|
||||
|
||||
Use [../../run_issue_data_inference.sh](../../run_issue_data_inference.sh) or [../../run_issue_data_inference.py](../../run_issue_data_inference.py).
|
||||
|
||||
```bash
|
||||
DRY_RUN=1 bash ../../run_issue_data_inference.sh
|
||||
```
|
||||
|
||||
```bash
|
||||
bash ../../run_issue_data_inference.sh
|
||||
```
|
||||
|
||||
Behavior:
|
||||
|
||||
- recursively scans the download root for `*/sigmastar.1/camera4.bin`
|
||||
- calls [../../../model_inference/core/run_two_roi_exported_onnx_infer.py](../../../model_inference/core/run_two_roi_exported_onnx_infer.py) with `--video-case-dir`
|
||||
- mirrors the download-tree relative layout into the inference output root
|
||||
|
||||
Defaults:
|
||||
|
||||
- inference root: `/data1/dongying/Mono3d/G1Q3/feishu_project/inference_issue_data`
|
||||
- manifest: `<inference_root>/inference_manifest.json`
|
||||
|
||||
Useful flags:
|
||||
|
||||
- `SKIP_EXISTING=1`
|
||||
- `ENABLE_ATTR=1`
|
||||
- `SAVE_AGGREGATE_PREDICTIONS=1`
|
||||
- `VIDEO_STRIDE=<n>`
|
||||
- `MAX_IMAGES=<n>`
|
||||
|
||||
## Current repo artifacts
|
||||
|
||||
These files are useful outputs, but they are not the source of truth for latest Feishu data:
|
||||
|
||||
- [../../dongying_g1q3_issue_list.json](../../dongying_g1q3_issue_list.json)
|
||||
- [../../dongying_g1q3_data_address_summary.md](../../dongying_g1q3_data_address_summary.md)
|
||||
- [../../dongying_g1q3_data_address_catalog.md](../../dongying_g1q3_data_address_catalog.md)
|
||||
|
||||
If the user asks for latest status, re-query Feishu with `fp` and regenerate outputs instead of trusting stale local exports.
|
||||
1274
tools/feishu_project/analyze_issue_tag_profile.py
Executable file
1274
tools/feishu_project/analyze_issue_tag_profile.py
Executable file
File diff suppressed because it is too large
Load Diff
221
tools/feishu_project/build_cncap_case_issue_json.py
Executable file
221
tools/feishu_project/build_cncap_case_issue_json.py
Executable file
@@ -0,0 +1,221 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build a synthetic issue JSON from a plain CNCAP case list."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from collections import Counter
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
DEFAULT_INPUT = Path(__file__).with_name("cncap_case.txt")
|
||||
DEFAULT_ID_BASE = 9_000_000_000
|
||||
PDCL_REF_RE = re.compile(r"ADAS_[^:/\\\s]+::[^/\\\s]*")
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--input",
|
||||
type=Path,
|
||||
default=DEFAULT_INPUT,
|
||||
help="Path to the CNCAP case list text file.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
type=Path,
|
||||
required=True,
|
||||
help="Path to the synthetic issue JSON output.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--case-index-output",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Optional companion index JSON output path.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--id-base",
|
||||
type=int,
|
||||
default=DEFAULT_ID_BASE,
|
||||
help="Synthetic issue id base. The first generated issue id is id_base + 1.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def parse_case_line(raw_line: str, line_no: int) -> tuple[str, str] | None:
|
||||
stripped = raw_line.strip()
|
||||
if not stripped or stripped.startswith("#"):
|
||||
return None
|
||||
|
||||
if "|" in stripped:
|
||||
case_name, source = (part.strip() for part in stripped.split("|", 1))
|
||||
else:
|
||||
try:
|
||||
case_name, source = stripped.rsplit(None, 1)
|
||||
except ValueError as exc:
|
||||
raise ValueError(
|
||||
f"Line {line_no}: expected '<case_name> <case_ref>' or '<case_name> | <case_ref>', "
|
||||
f"got: {raw_line.rstrip()!r}"
|
||||
) from exc
|
||||
|
||||
if not case_name or not source:
|
||||
raise ValueError(f"Line {line_no}: empty case name or case reference")
|
||||
return case_name, source
|
||||
|
||||
|
||||
def classify_source(source: str, line_no: int) -> tuple[str, str, str]:
|
||||
if "mdi raw" in source.lower() or PDCL_REF_RE.search(source):
|
||||
return "", source, "pdcl_mdi_download"
|
||||
if "/" in source or "\\" in source:
|
||||
return source, "", "standard_path"
|
||||
raise ValueError(f"Line {line_no}: unsupported case reference format: {source!r}")
|
||||
|
||||
|
||||
def build_payload(
|
||||
*,
|
||||
input_path: Path,
|
||||
case_rows: list[dict[str, Any]],
|
||||
id_base: int,
|
||||
) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
exported_at = datetime.now().astimezone().isoformat(timespec="seconds")
|
||||
name_counter: Counter[str] = Counter()
|
||||
items: list[dict[str, Any]] = []
|
||||
index_cases: list[dict[str, Any]] = []
|
||||
source_kind_counts: Counter[str] = Counter()
|
||||
|
||||
for ordinal, row in enumerate(case_rows, start=1):
|
||||
case_name = row["case_name"]
|
||||
source = row["source"]
|
||||
line_no = row["line_no"]
|
||||
raw_line = row["raw_line"]
|
||||
name_counter[case_name] += 1
|
||||
occurrence_index = name_counter[case_name]
|
||||
issue_id = id_base + ordinal
|
||||
|
||||
standard_path, pdcl_ref, source_kind = classify_source(source, line_no)
|
||||
source_kind_counts[source_kind] += 1
|
||||
|
||||
item = {
|
||||
"id": issue_id,
|
||||
"name": case_name,
|
||||
"status": "CNCAP_CASE",
|
||||
"created_at": None,
|
||||
"updated_at": exported_at,
|
||||
"问题数据地址": standard_path,
|
||||
"问题数据地址_PDCL": pdcl_ref,
|
||||
"问题发生frameid": None,
|
||||
"source_line_no": line_no,
|
||||
"case_ref": source,
|
||||
"case_occurrence_index": occurrence_index,
|
||||
"synthetic_issue_key": f"cncap_case_{ordinal:03d}",
|
||||
}
|
||||
items.append(item)
|
||||
index_cases.append(
|
||||
{
|
||||
"id": issue_id,
|
||||
"name": case_name,
|
||||
"source_kind": source_kind,
|
||||
"source_line_no": line_no,
|
||||
"case_ref": source,
|
||||
"case_occurrence_index": occurrence_index,
|
||||
"raw_line": raw_line,
|
||||
}
|
||||
)
|
||||
|
||||
duplicate_names = sorted(name for name, count in name_counter.items() if count > 1)
|
||||
payload = {
|
||||
"exported_at": exported_at,
|
||||
"source": {
|
||||
"type": "cncap_case_txt",
|
||||
"input_path": str(input_path.resolve()),
|
||||
"id_base": id_base,
|
||||
"work_item_type": "synthetic_issue",
|
||||
"included_detail_fields": [
|
||||
"问题数据地址",
|
||||
"问题数据地址_PDCL",
|
||||
"问题发生frameid",
|
||||
],
|
||||
},
|
||||
"summary": {
|
||||
"success": True,
|
||||
"total": len(items),
|
||||
"duplicate_name_count": len(duplicate_names),
|
||||
"pdcl_case_count": source_kind_counts.get("pdcl_mdi_download", 0),
|
||||
"standard_path_case_count": source_kind_counts.get("standard_path", 0),
|
||||
},
|
||||
"items": items,
|
||||
}
|
||||
case_index = {
|
||||
"generated_at": exported_at,
|
||||
"input_path": str(input_path.resolve()),
|
||||
"id_base": id_base,
|
||||
"total_cases": len(index_cases),
|
||||
"duplicate_names": duplicate_names,
|
||||
"cases": index_cases,
|
||||
}
|
||||
return payload, case_index
|
||||
|
||||
|
||||
def write_json(path: Path, payload: dict[str, Any]) -> 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 main() -> int:
|
||||
args = parse_args()
|
||||
input_path = args.input.resolve()
|
||||
output_path = args.output.resolve()
|
||||
case_index_output = (
|
||||
args.case_index_output.resolve()
|
||||
if args.case_index_output is not None
|
||||
else output_path.with_name(f"{output_path.stem}.case_index.json")
|
||||
)
|
||||
|
||||
if args.id_base < 0:
|
||||
raise ValueError("--id-base must be greater than or equal to 0")
|
||||
if not input_path.is_file():
|
||||
raise FileNotFoundError(f"Input case list not found: {input_path}")
|
||||
|
||||
case_rows: list[dict[str, Any]] = []
|
||||
for line_no, raw_line in enumerate(input_path.read_text(encoding="utf-8").splitlines(), start=1):
|
||||
parsed = parse_case_line(raw_line, line_no)
|
||||
if parsed is None:
|
||||
continue
|
||||
case_name, source = parsed
|
||||
case_rows.append(
|
||||
{
|
||||
"line_no": line_no,
|
||||
"raw_line": raw_line,
|
||||
"case_name": case_name,
|
||||
"source": source,
|
||||
}
|
||||
)
|
||||
|
||||
if not case_rows:
|
||||
raise ValueError(f"No runnable cases were found in {input_path}")
|
||||
|
||||
payload, case_index = build_payload(
|
||||
input_path=input_path,
|
||||
case_rows=case_rows,
|
||||
id_base=args.id_base,
|
||||
)
|
||||
write_json(output_path, payload)
|
||||
write_json(case_index_output, case_index)
|
||||
|
||||
print(f"input: {input_path}")
|
||||
print(f"output: {output_path}")
|
||||
print(f"case_index_output: {case_index_output}")
|
||||
print(f"total_cases: {payload['summary']['total']}")
|
||||
print(f"duplicate_name_count: {payload['summary']['duplicate_name_count']}")
|
||||
print(f"pdcl_case_count: {payload['summary']['pdcl_case_count']}")
|
||||
print(f"standard_path_case_count: {payload['summary']['standard_path_case_count']}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
326
tools/feishu_project/case_calib_recovery.py
Executable file
326
tools/feishu_project/case_calib_recovery.py
Executable file
@@ -0,0 +1,326 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Recover camera4.json for downloaded standard-path issue cases."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable, Optional
|
||||
|
||||
|
||||
TARGET_CALIB_REL = Path("test_data") / "calibs" / "camera4.json"
|
||||
CALIB_ATTACHMENT_NAMES = (
|
||||
"sigmastar.1/calibs/camera4.json",
|
||||
"test_data/calibs/camera4.json",
|
||||
"calibs/camera4.json",
|
||||
)
|
||||
CAMERA_CONFIG_FILE_NAME = "camera_config_folder.bin"
|
||||
PREFERRED_CAMERA_IDS = (4, 0)
|
||||
REQUIRED_FLAT_CALIB_KEYS = ("focal_u", "focal_v", "cu", "cv")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ExtractedCalibSource:
|
||||
payload: dict[str, Any]
|
||||
source_kind: str
|
||||
source_path: Path
|
||||
detail: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CalibRecoveryResult:
|
||||
status: str
|
||||
detail: str
|
||||
target_path: Path
|
||||
source_path: Path | None = None
|
||||
source_kind: str | None = None
|
||||
|
||||
|
||||
def _safe_int(value: Any) -> int | None:
|
||||
try:
|
||||
if value is None:
|
||||
return None
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _sorted_unique_paths(paths: Iterable[Path]) -> list[Path]:
|
||||
unique = {path.resolve() if path.exists() else path for path in paths}
|
||||
return sorted(unique, key=lambda path: (len(path.parts), str(path)))
|
||||
|
||||
|
||||
def _camera_attachment_matches(name: str) -> bool:
|
||||
normalized = str(name).strip().replace("\\", "/")
|
||||
if normalized in CALIB_ATTACHMENT_NAMES:
|
||||
return True
|
||||
return normalized.endswith("/camera4.json") or normalized == "camera4.json"
|
||||
|
||||
|
||||
def iter_concatenated_json_objects(text: str) -> list[dict[str, Any]]:
|
||||
decoder = json.JSONDecoder()
|
||||
pos = 0
|
||||
payloads: list[dict[str, Any]] = []
|
||||
text_len = len(text)
|
||||
|
||||
while pos < text_len:
|
||||
while pos < text_len and (text[pos].isspace() or text[pos] == "\x00"):
|
||||
pos += 1
|
||||
if pos >= text_len:
|
||||
break
|
||||
|
||||
obj, end = decoder.raw_decode(text, pos)
|
||||
if isinstance(obj, dict):
|
||||
payloads.append(obj)
|
||||
pos = end
|
||||
|
||||
return payloads
|
||||
|
||||
|
||||
def _coerce_float_list(value: Any, *, min_len: int = 0) -> list[float]:
|
||||
if isinstance(value, dict):
|
||||
ordered = [value.get("x"), value.get("y"), value.get("z")]
|
||||
result = [float(item) for item in ordered if item is not None]
|
||||
elif isinstance(value, (list, tuple)):
|
||||
result = [float(item) for item in value]
|
||||
else:
|
||||
result = []
|
||||
while len(result) < min_len:
|
||||
result.append(0.0)
|
||||
return result
|
||||
|
||||
|
||||
def _is_valid_flat_calib_payload(payload: dict[str, Any]) -> bool:
|
||||
return all(payload.get(key) is not None for key in REQUIRED_FLAT_CALIB_KEYS)
|
||||
|
||||
|
||||
def _build_flat_camera4_payload(record: dict[str, Any], source_path: Path) -> dict[str, Any]:
|
||||
payload = dict(record)
|
||||
payload["focal_u"] = float(record["focal_u"])
|
||||
payload["focal_v"] = float(record["focal_v"])
|
||||
payload["cu"] = float(record["cu"])
|
||||
payload["cv"] = float(record["cv"])
|
||||
payload["roll"] = float(record.get("roll", 0.0))
|
||||
payload["pitch"] = float(record.get("pitch", 0.0))
|
||||
payload["yaw"] = float(record.get("yaw", 0.0))
|
||||
payload["distort_coeffs"] = _coerce_float_list(record.get("distort_coeffs"))
|
||||
payload["pos"] = _coerce_float_list(record.get("pos"), min_len=3)[:3]
|
||||
if payload.get("image_width") is not None:
|
||||
payload["image_width"] = int(payload["image_width"])
|
||||
if payload.get("image_height") is not None:
|
||||
payload["image_height"] = int(payload["image_height"])
|
||||
payload.setdefault("camera_id", 4)
|
||||
payload["recovered_from"] = CAMERA_CONFIG_FILE_NAME
|
||||
payload["recovered_from_path"] = str(source_path)
|
||||
return payload
|
||||
|
||||
|
||||
def extract_calib_from_camera_config_folder(
|
||||
config_path: Path,
|
||||
preferred_camera_ids: tuple[int, ...] = PREFERRED_CAMERA_IDS,
|
||||
) -> ExtractedCalibSource | None:
|
||||
text = config_path.read_text(encoding="utf-8")
|
||||
records = iter_concatenated_json_objects(text)
|
||||
valid_records = [record for record in records if _is_valid_flat_calib_payload(record)]
|
||||
if not valid_records:
|
||||
return None
|
||||
|
||||
selected_records = valid_records
|
||||
available_camera_ids = {
|
||||
_safe_int(record.get("camera_id"))
|
||||
for record in valid_records
|
||||
if _safe_int(record.get("camera_id")) is not None
|
||||
}
|
||||
chosen_camera_id: int | None = None
|
||||
|
||||
for camera_id in preferred_camera_ids:
|
||||
preferred_records = [
|
||||
record for record in valid_records if _safe_int(record.get("camera_id")) == camera_id
|
||||
]
|
||||
if preferred_records:
|
||||
selected_records = preferred_records
|
||||
chosen_camera_id = camera_id
|
||||
break
|
||||
|
||||
if chosen_camera_id is None:
|
||||
if len(available_camera_ids) == 1:
|
||||
chosen_camera_id = next(iter(available_camera_ids))
|
||||
else:
|
||||
counts: dict[int, int] = {}
|
||||
for record in valid_records:
|
||||
camera_id = _safe_int(record.get("camera_id"))
|
||||
if camera_id is None:
|
||||
continue
|
||||
counts[camera_id] = counts.get(camera_id, 0) + 1
|
||||
if counts:
|
||||
chosen_camera_id = max(counts.items(), key=lambda item: item[1])[0]
|
||||
selected_records = [
|
||||
record for record in valid_records if _safe_int(record.get("camera_id")) == chosen_camera_id
|
||||
]
|
||||
|
||||
best_record = max(
|
||||
enumerate(selected_records),
|
||||
key=lambda item: (_safe_int(item[1].get("utc_tick")) or -1, item[0]),
|
||||
)[1]
|
||||
payload = _build_flat_camera4_payload(best_record, config_path)
|
||||
detail = (
|
||||
f"camera_id={chosen_camera_id if chosen_camera_id is not None else 'unknown'} "
|
||||
f"records={len(valid_records)} latest_utc_tick={best_record.get('utc_tick')}"
|
||||
)
|
||||
return ExtractedCalibSource(
|
||||
payload=payload,
|
||||
source_kind="camera_config_folder",
|
||||
source_path=config_path,
|
||||
detail=detail,
|
||||
)
|
||||
|
||||
|
||||
def _extract_calib_from_mcap_with_clip_reader(mcap_path: Path) -> ExtractedCalibSource | None:
|
||||
try:
|
||||
from pdcl_pyclip.reader import ClipReader
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
reader = ClipReader(str(mcap_path))
|
||||
for attachment in reader.iter_attachments():
|
||||
if _camera_attachment_matches(attachment.name):
|
||||
payload = json.loads(attachment.data.decode("utf-8"))
|
||||
return ExtractedCalibSource(
|
||||
payload=payload,
|
||||
source_kind="mcap_attachment",
|
||||
source_path=mcap_path,
|
||||
detail=f"attachment={attachment.name}",
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def _extract_calib_from_mcap_with_generic_reader(mcap_path: Path) -> ExtractedCalibSource | None:
|
||||
try:
|
||||
from mcap.reader import make_reader
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
with mcap_path.open("rb") as file:
|
||||
reader = make_reader(file)
|
||||
for attachment in reader.iter_attachments():
|
||||
if _camera_attachment_matches(attachment.name):
|
||||
payload = json.loads(attachment.data.decode("utf-8"))
|
||||
return ExtractedCalibSource(
|
||||
payload=payload,
|
||||
source_kind="mcap_attachment",
|
||||
source_path=mcap_path,
|
||||
detail=f"attachment={attachment.name}",
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def extract_calib_from_mcap_attachment(mcap_path: Path) -> ExtractedCalibSource | None:
|
||||
errors: list[str] = []
|
||||
for extractor in (_extract_calib_from_mcap_with_clip_reader, _extract_calib_from_mcap_with_generic_reader):
|
||||
try:
|
||||
extracted = extractor(mcap_path)
|
||||
except Exception as exc: # pragma: no cover - recovery fallback logging
|
||||
errors.append(f"{extractor.__name__}: {type(exc).__name__}: {exc}")
|
||||
continue
|
||||
if extracted is not None:
|
||||
return extracted
|
||||
|
||||
if errors:
|
||||
raise RuntimeError("; ".join(errors))
|
||||
return None
|
||||
|
||||
|
||||
def _find_candidate_camera_config_paths(source_root: Path) -> list[Path]:
|
||||
return _sorted_unique_paths(source_root.rglob(CAMERA_CONFIG_FILE_NAME))
|
||||
|
||||
|
||||
def _find_candidate_mcap_paths(source_root: Path) -> list[Path]:
|
||||
return _sorted_unique_paths(source_root.rglob("*.mcap"))
|
||||
|
||||
|
||||
def _write_camera4_json(target_path: Path, payload: dict[str, Any]) -> None:
|
||||
target_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
target_path.write_text(
|
||||
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def recover_camera4_json(
|
||||
source_root: Path,
|
||||
target_root: Path,
|
||||
dry_run: bool = False,
|
||||
) -> CalibRecoveryResult:
|
||||
target_path = target_root / TARGET_CALIB_REL
|
||||
if target_path.is_file():
|
||||
return CalibRecoveryResult(
|
||||
status="skipped_existing_calib",
|
||||
detail=f"target already exists: {target_path}",
|
||||
target_path=target_path,
|
||||
)
|
||||
|
||||
camera_config_paths = _find_candidate_camera_config_paths(source_root)
|
||||
mcap_paths = _find_candidate_mcap_paths(source_root)
|
||||
errors: list[str] = []
|
||||
|
||||
for config_path in camera_config_paths:
|
||||
try:
|
||||
extracted = extract_calib_from_camera_config_folder(config_path)
|
||||
except Exception as exc:
|
||||
errors.append(f"{config_path}: {type(exc).__name__}: {exc}")
|
||||
continue
|
||||
if extracted is None:
|
||||
continue
|
||||
if dry_run:
|
||||
return CalibRecoveryResult(
|
||||
status="planned_calib_recovery_from_camera_config_folder",
|
||||
detail=f"would write {target_path} from {config_path} ({extracted.detail})",
|
||||
target_path=target_path,
|
||||
source_path=config_path,
|
||||
source_kind=extracted.source_kind,
|
||||
)
|
||||
_write_camera4_json(target_path, extracted.payload)
|
||||
return CalibRecoveryResult(
|
||||
status="recovered_calib_from_camera_config_folder",
|
||||
detail=f"wrote {target_path} from {config_path} ({extracted.detail})",
|
||||
target_path=target_path,
|
||||
source_path=config_path,
|
||||
source_kind=extracted.source_kind,
|
||||
)
|
||||
|
||||
for mcap_path in mcap_paths:
|
||||
try:
|
||||
extracted = extract_calib_from_mcap_attachment(mcap_path)
|
||||
except Exception as exc:
|
||||
errors.append(f"{mcap_path}: {type(exc).__name__}: {exc}")
|
||||
continue
|
||||
if extracted is None:
|
||||
continue
|
||||
if dry_run:
|
||||
return CalibRecoveryResult(
|
||||
status="planned_calib_recovery_from_mcap_attachment",
|
||||
detail=f"would write {target_path} from {mcap_path} ({extracted.detail})",
|
||||
target_path=target_path,
|
||||
source_path=mcap_path,
|
||||
source_kind=extracted.source_kind,
|
||||
)
|
||||
_write_camera4_json(target_path, extracted.payload)
|
||||
return CalibRecoveryResult(
|
||||
status="recovered_calib_from_mcap_attachment",
|
||||
detail=f"wrote {target_path} from {mcap_path} ({extracted.detail})",
|
||||
target_path=target_path,
|
||||
source_path=mcap_path,
|
||||
source_kind=extracted.source_kind,
|
||||
)
|
||||
|
||||
checked_sources = (
|
||||
f"camera_config_folder.bin={len(camera_config_paths)} mcap={len(mcap_paths)}"
|
||||
)
|
||||
if errors:
|
||||
checked_sources += f"; errors: {' | '.join(errors[:3])}"
|
||||
return CalibRecoveryResult(
|
||||
status="failed_calib_recovery",
|
||||
detail=f"no recoverable calibration found under {source_root} ({checked_sources})",
|
||||
target_path=target_path,
|
||||
)
|
||||
9
tools/feishu_project/cncap_case.txt
Executable file
9
tools/feishu_project/cncap_case.txt
Executable file
@@ -0,0 +1,9 @@
|
||||
CPLA-夜晚_AEB_40_5 ADAS_S5STNF0T406280R_20260409152111002995_edda23ef80bd::20260408001902
|
||||
CSTA-LN_AEB_10_20 ADAS_S5STNF0T406280R_20260415153614565776_6304fc524d71::20260414173141
|
||||
CSTA-LN_AEB_10_20 ADAS_S5STNF0T406280R_20260415153614565776_7b6bcdb90977::20260414172819
|
||||
CSTA-LN_AEB_20_20 ADAS_S5STNF0T406280R_20260415153614565776_0ae4e25121ec::20260414171833
|
||||
CSTA-LN_AEB_20_20 ADAS_S5STNF0T406280R_20260415153614565776_c66607e24083::20260414170603
|
||||
CPLA-夜晚_AEB_FCW70_5 ADAS_S5STNF0T406280R_20260409152111002995_dd98b096d792::20260407233549
|
||||
CPLA-夜晚_AEB_FCW60_5 ADAS_S5STNF0T406280R_20260409152111002995_8065a4b59db6::20260407233159
|
||||
CBLA_AEB_FCW80_15 ADAS_S5STNF0T406280R_20260409152111002995_7254e457ef67::20260407115528
|
||||
CBLA_AEB_FCW70_15 ADAS_S5STNF0T406280R_20260409152111002995_44c27781ddc3::20260407114731
|
||||
838
tools/feishu_project/decode_issue_frame_window.py
Executable file
838
tools/feishu_project/decode_issue_frame_window.py
Executable file
@@ -0,0 +1,838 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Decode 61-frame windows around issue frame ids for D4Q2 network-share cases."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from collections import Counter, deque
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable, Optional
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
FILE = Path(__file__).resolve()
|
||||
ROOT = FILE.parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.append(str(ROOT))
|
||||
|
||||
from tools.model_inference.adapters.video_dir_inference_utils import (
|
||||
get_video_frame_info,
|
||||
read_video_frame_index,
|
||||
)
|
||||
|
||||
try:
|
||||
from pdcl_pyclip.decoder_struct import StructDecoder
|
||||
from pdcl_pyclip.msg_camera import VideoMessage
|
||||
from pdcl_pyclip.reader import ClipReader
|
||||
except ImportError:
|
||||
ClipReader = None
|
||||
StructDecoder = None
|
||||
VideoMessage = None
|
||||
|
||||
|
||||
NETWORK_SHARE_MARKERS = ("hfs.minieye.tech", "192.168.2.122")
|
||||
WINDOW_RADIUS = 30
|
||||
WINDOW_SIZE = WINDOW_RADIUS * 2 + 1
|
||||
DEFAULT_INPUT_JSON = ROOT / "tools" / "feishu_project" / "dongying_d4q2_zhibao_issue_list.json"
|
||||
DEFAULT_DOWNLOAD_ROOT = Path("/data1/dongying/Mono3d/D4Q2/feishu_project/downloaded_issue_data")
|
||||
DEFAULT_OUTPUT_ROOT = Path("/data1/dongying/Mono3d/D4Q2/feishu_project/decoded_issue_frame_windows")
|
||||
FRAME_ID_CAMERA4_RE = re.compile(r"camera4\s*:\s*(\d+)", re.IGNORECASE)
|
||||
FRAME_ID_ANY_CAMERA_RE = re.compile(r"(camera\d+)\s*:\s*(\d+)", re.IGNORECASE)
|
||||
PURE_DIGIT_RE = re.compile(r"^\d+$")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TargetFrame:
|
||||
camera: str
|
||||
frame_id: int
|
||||
raw_text: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CaseSource:
|
||||
issue_id: int
|
||||
issue_name: str
|
||||
issue_dir: Path
|
||||
path_dir: Path
|
||||
case_dir: Path
|
||||
relative_case_dir: Path
|
||||
target_frame: TargetFrame
|
||||
decode_all: bool
|
||||
source_mode: str
|
||||
source_paths: tuple[Path, ...]
|
||||
|
||||
|
||||
@dataclass
|
||||
class CaseResult:
|
||||
issue_id: int
|
||||
issue_name: str
|
||||
case_dir: str
|
||||
relative_case_dir: str
|
||||
target_camera: str
|
||||
target_frame_id: int | None
|
||||
decode_mode: str
|
||||
source_mode: str | None
|
||||
source_paths: list[str]
|
||||
output_dir: str
|
||||
status: str
|
||||
detail: str
|
||||
matched_field: str | None = None
|
||||
matched_frame_idx: int | None = None
|
||||
matched_topic: str | None = None
|
||||
extracted_count: int = 0
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"issue_id": self.issue_id,
|
||||
"issue_name": self.issue_name,
|
||||
"case_dir": self.case_dir,
|
||||
"relative_case_dir": self.relative_case_dir,
|
||||
"target_camera": self.target_camera,
|
||||
"target_frame_id": self.target_frame_id,
|
||||
"decode_mode": self.decode_mode,
|
||||
"source_mode": self.source_mode,
|
||||
"source_paths": self.source_paths,
|
||||
"output_dir": self.output_dir,
|
||||
"status": self.status,
|
||||
"detail": self.detail,
|
||||
"matched_field": self.matched_field,
|
||||
"matched_frame_idx": self.matched_frame_idx,
|
||||
"matched_topic": self.matched_topic,
|
||||
"extracted_count": self.extracted_count,
|
||||
}
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Decode 61-frame windows around issue frame ids for downloaded D4Q2 network-share cases."
|
||||
)
|
||||
parser.add_argument("--input-json", default=str(DEFAULT_INPUT_JSON))
|
||||
parser.add_argument("--download-root", default=str(DEFAULT_DOWNLOAD_ROOT))
|
||||
parser.add_argument("--output-root", default=str(DEFAULT_OUTPUT_ROOT))
|
||||
parser.add_argument("--manifest-path", default="")
|
||||
parser.add_argument("--issue-id", action="append", dest="issue_ids", type=int)
|
||||
parser.add_argument("--decode-all-issue-id", action="append", dest="decode_all_issue_ids", type=int)
|
||||
parser.add_argument("--window-radius", type=int, default=WINDOW_RADIUS)
|
||||
parser.add_argument("--jpg-quality", type=int, default=95)
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
parser.add_argument("--skip-existing", action="store_true")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def ensure_dir(path: Path, dry_run: bool) -> None:
|
||||
if dry_run:
|
||||
return
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def load_issue_items(path: Path) -> list[dict[str, Any]]:
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
return payload["items"]
|
||||
|
||||
|
||||
def parse_target_frame(frame_text: object) -> TargetFrame | None:
|
||||
text = "" if frame_text is None else str(frame_text).strip()
|
||||
if not text:
|
||||
return None
|
||||
camera4_match = FRAME_ID_CAMERA4_RE.search(text)
|
||||
if camera4_match:
|
||||
return TargetFrame(camera="camera4", frame_id=int(camera4_match.group(1)), raw_text=text)
|
||||
if PURE_DIGIT_RE.fullmatch(text):
|
||||
return TargetFrame(camera="any", frame_id=int(text), raw_text=text)
|
||||
any_camera_match = FRAME_ID_ANY_CAMERA_RE.search(text)
|
||||
if any_camera_match:
|
||||
return TargetFrame(camera=any_camera_match.group(1).lower(), frame_id=int(any_camera_match.group(2)), raw_text=text)
|
||||
return None
|
||||
|
||||
|
||||
def is_network_share_issue(item: dict[str, Any]) -> bool:
|
||||
address = str(item.get("问题数据地址") or "")
|
||||
return any(marker in address for marker in NETWORK_SHARE_MARKERS)
|
||||
|
||||
|
||||
def find_candidate_mcaps(case_dir: Path) -> list[Path]:
|
||||
root_level = sorted(
|
||||
path
|
||||
for path in case_dir.iterdir()
|
||||
if path.is_file() and path.suffix.lower() == ".mcap" and "_PB_" not in path.name
|
||||
)
|
||||
if root_level:
|
||||
return root_level
|
||||
|
||||
recursive = sorted(
|
||||
path
|
||||
for path in case_dir.rglob("*.mcap")
|
||||
if path.is_file() and "_PB_" not in path.name
|
||||
)
|
||||
return recursive
|
||||
|
||||
|
||||
def find_camera4_bin(case_dir: Path) -> Path | None:
|
||||
candidates = sorted(case_dir.rglob("camera4.bin"), key=lambda p: (len(p.relative_to(case_dir).parts), str(p)))
|
||||
return candidates[0] if candidates else None
|
||||
|
||||
|
||||
def discover_case_sources(
|
||||
items: list[dict[str, Any]],
|
||||
download_root: Path,
|
||||
issue_filter: set[int] | None,
|
||||
decode_all_issue_filter: set[int],
|
||||
) -> tuple[list[CaseSource], list[CaseResult]]:
|
||||
discovered: list[CaseSource] = []
|
||||
skipped: list[CaseResult] = []
|
||||
|
||||
for item in items:
|
||||
issue_id = int(item["id"])
|
||||
if issue_filter and issue_id not in issue_filter:
|
||||
continue
|
||||
if not is_network_share_issue(item):
|
||||
continue
|
||||
|
||||
issue_name = str(item["name"])
|
||||
issue_dir = download_root / f"issue_{issue_id}"
|
||||
target_frame = parse_target_frame(item.get("问题发生frameid"))
|
||||
if target_frame is None:
|
||||
skipped.append(
|
||||
CaseResult(
|
||||
issue_id=issue_id,
|
||||
issue_name=issue_name,
|
||||
case_dir="",
|
||||
relative_case_dir="",
|
||||
target_camera="",
|
||||
target_frame_id=None,
|
||||
decode_mode="window",
|
||||
source_mode=None,
|
||||
source_paths=[],
|
||||
output_dir="",
|
||||
status="skipped_missing_frame_id",
|
||||
detail=f"unparseable frame id: {item.get('问题发生frameid')!r}",
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
if target_frame.camera not in {"camera4", "camera1", "any"}:
|
||||
skipped.append(
|
||||
CaseResult(
|
||||
issue_id=issue_id,
|
||||
issue_name=issue_name,
|
||||
case_dir="",
|
||||
relative_case_dir="",
|
||||
target_camera=target_frame.camera,
|
||||
target_frame_id=target_frame.frame_id,
|
||||
decode_mode="all" if issue_id in decode_all_issue_filter else "window",
|
||||
source_mode=None,
|
||||
source_paths=[],
|
||||
output_dir="",
|
||||
status="skipped_unsupported_camera",
|
||||
detail=f"unsupported camera selector in frame id: {target_frame.raw_text}",
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
if not issue_dir.is_dir():
|
||||
skipped.append(
|
||||
CaseResult(
|
||||
issue_id=issue_id,
|
||||
issue_name=issue_name,
|
||||
case_dir=str(issue_dir),
|
||||
relative_case_dir=str(issue_dir.name),
|
||||
target_camera=target_frame.camera,
|
||||
target_frame_id=target_frame.frame_id,
|
||||
decode_mode="all" if issue_id in decode_all_issue_filter else "window",
|
||||
source_mode=None,
|
||||
source_paths=[],
|
||||
output_dir="",
|
||||
status="skipped_missing_issue_dir",
|
||||
detail=f"issue download directory not found: {issue_dir}",
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
path_dirs = sorted(path for path in issue_dir.iterdir() if path.is_dir() and path.name.startswith("path_"))
|
||||
if not path_dirs:
|
||||
skipped.append(
|
||||
CaseResult(
|
||||
issue_id=issue_id,
|
||||
issue_name=issue_name,
|
||||
case_dir=str(issue_dir),
|
||||
relative_case_dir=str(issue_dir.name),
|
||||
target_camera=target_frame.camera,
|
||||
target_frame_id=target_frame.frame_id,
|
||||
decode_mode="all" if issue_id in decode_all_issue_filter else "window",
|
||||
source_mode=None,
|
||||
source_paths=[],
|
||||
output_dir="",
|
||||
status="skipped_no_path_cases",
|
||||
detail="no path_* directories found for network-share issue",
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
for path_dir in path_dirs:
|
||||
case_dirs = sorted(path for path in path_dir.iterdir() if path.is_dir())
|
||||
if not case_dirs:
|
||||
skipped.append(
|
||||
CaseResult(
|
||||
issue_id=issue_id,
|
||||
issue_name=issue_name,
|
||||
case_dir=str(path_dir),
|
||||
relative_case_dir=str(path_dir.relative_to(download_root)),
|
||||
target_camera=target_frame.camera,
|
||||
target_frame_id=target_frame.frame_id,
|
||||
decode_mode="all" if issue_id in decode_all_issue_filter else "window",
|
||||
source_mode=None,
|
||||
source_paths=[],
|
||||
output_dir="",
|
||||
status="skipped_empty_path_dir",
|
||||
detail="path_* directory does not contain any case subdirectory",
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
for case_dir in case_dirs:
|
||||
relative_case_dir = case_dir.relative_to(download_root)
|
||||
candidate_mcaps = find_candidate_mcaps(case_dir)
|
||||
if candidate_mcaps:
|
||||
discovered.append(
|
||||
CaseSource(
|
||||
issue_id=issue_id,
|
||||
issue_name=issue_name,
|
||||
issue_dir=issue_dir,
|
||||
path_dir=path_dir,
|
||||
case_dir=case_dir,
|
||||
relative_case_dir=relative_case_dir,
|
||||
target_frame=target_frame,
|
||||
decode_all=issue_id in decode_all_issue_filter,
|
||||
source_mode="mcap_stream",
|
||||
source_paths=tuple(candidate_mcaps),
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
camera4_bin = find_camera4_bin(case_dir)
|
||||
if camera4_bin is not None:
|
||||
discovered.append(
|
||||
CaseSource(
|
||||
issue_id=issue_id,
|
||||
issue_name=issue_name,
|
||||
issue_dir=issue_dir,
|
||||
path_dir=path_dir,
|
||||
case_dir=case_dir,
|
||||
relative_case_dir=relative_case_dir,
|
||||
target_frame=target_frame,
|
||||
decode_all=issue_id in decode_all_issue_filter,
|
||||
source_mode="camera4_bin",
|
||||
source_paths=(camera4_bin,),
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
skipped.append(
|
||||
CaseResult(
|
||||
issue_id=issue_id,
|
||||
issue_name=issue_name,
|
||||
case_dir=str(case_dir),
|
||||
relative_case_dir=str(relative_case_dir),
|
||||
target_camera=target_frame.camera,
|
||||
target_frame_id=target_frame.frame_id,
|
||||
decode_mode="all" if issue_id in decode_all_issue_filter else "window",
|
||||
source_mode=None,
|
||||
source_paths=[],
|
||||
output_dir="",
|
||||
status="skipped_no_source",
|
||||
detail="no non-PB .mcap or camera4.bin found under case directory",
|
||||
)
|
||||
)
|
||||
return discovered, skipped
|
||||
|
||||
|
||||
def _plane_to_ndarray(plane) -> np.ndarray:
|
||||
stride = plane.line_size
|
||||
height = plane.height
|
||||
width = plane.width
|
||||
array = np.frombuffer(plane, dtype=np.uint8)
|
||||
if stride == width:
|
||||
return array.reshape(height, width)
|
||||
return array.reshape(height, stride)[:, :width]
|
||||
|
||||
|
||||
def _h265_payload_to_bgr(payload: bytes) -> np.ndarray:
|
||||
try:
|
||||
import av
|
||||
except ImportError as exc:
|
||||
raise ImportError("PyAV is required for MCAP frame decoding.") from exc
|
||||
|
||||
container = av.open(io.BytesIO(payload))
|
||||
for frame in container.decode(video=0):
|
||||
y_plane = _plane_to_ndarray(frame.planes[0])
|
||||
u_plane = _plane_to_ndarray(frame.planes[1])
|
||||
v_plane = _plane_to_ndarray(frame.planes[2])
|
||||
uv_plane = np.zeros((u_plane.shape[0], u_plane.shape[1] * 2), dtype=np.uint8)
|
||||
uv_plane[:, 0::2] = u_plane
|
||||
uv_plane[:, 1::2] = v_plane
|
||||
yuv_image = np.concatenate((y_plane.copy(), uv_plane), axis=0)
|
||||
return cv2.cvtColor(yuv_image, cv2.COLOR_YUV2BGR_NV12)
|
||||
raise ValueError("decode failed: no video frame in payload")
|
||||
|
||||
|
||||
def iter_mcap_frames(mcap_paths: Iterable[Path], topic_candidates: tuple[str, ...]) -> Iterable[dict[str, Any]]:
|
||||
if ClipReader is None or StructDecoder is None or VideoMessage is None:
|
||||
raise ImportError("pdcl_pyclip is required for MCAP extraction.")
|
||||
|
||||
decoder = StructDecoder()
|
||||
for mcap_path in mcap_paths:
|
||||
reader = ClipReader(str(mcap_path))
|
||||
for schema, channel, msg in reader.iter_messages():
|
||||
data = decoder.decode(schema, channel, msg)
|
||||
if not isinstance(data, VideoMessage):
|
||||
continue
|
||||
frame_id = getattr(data, "frame_id", None)
|
||||
if frame_id is None:
|
||||
continue
|
||||
yield {
|
||||
"source_path": mcap_path,
|
||||
"topic": getattr(channel, "topic", ""),
|
||||
"frame_id": int(frame_id),
|
||||
"payload": data.payload,
|
||||
"timestamp": str(getattr(data, "timestamp", "")),
|
||||
}
|
||||
|
||||
|
||||
def topic_matches_camera(topic: str, target_camera: str) -> bool:
|
||||
normalized_topic = str(topic or "").lower()
|
||||
if target_camera == "any":
|
||||
return "camera" in normalized_topic
|
||||
return target_camera in normalized_topic
|
||||
|
||||
|
||||
def collect_mcap_window(
|
||||
mcap_paths: tuple[Path, ...],
|
||||
target_camera: str,
|
||||
target_frame_id: int,
|
||||
window_radius: int,
|
||||
) -> tuple[list[dict[str, Any]], str, str]:
|
||||
buffer_before: deque[dict[str, Any]] = deque(maxlen=window_radius)
|
||||
selected: list[dict[str, Any]] = []
|
||||
trailing_needed = window_radius
|
||||
found = False
|
||||
matched_topic = ""
|
||||
|
||||
for frame in iter_mcap_frames(mcap_paths, tuple()):
|
||||
if not topic_matches_camera(frame["topic"], target_camera):
|
||||
continue
|
||||
if not found:
|
||||
if frame["frame_id"] == target_frame_id:
|
||||
selected = list(buffer_before) + [frame]
|
||||
found = True
|
||||
matched_topic = str(frame["topic"])
|
||||
else:
|
||||
buffer_before.append(frame)
|
||||
else:
|
||||
selected.append(frame)
|
||||
trailing_needed -= 1
|
||||
if trailing_needed <= 0:
|
||||
break
|
||||
|
||||
if not found:
|
||||
raise FileNotFoundError(f"target frame_id {target_frame_id} not found in MCAP sources for camera selector {target_camera}")
|
||||
|
||||
return selected, "frame_id", matched_topic
|
||||
|
||||
|
||||
def collect_mcap_all_frames(
|
||||
mcap_paths: tuple[Path, ...],
|
||||
target_camera: str,
|
||||
) -> tuple[list[dict[str, Any]], str]:
|
||||
selected: list[dict[str, Any]] = []
|
||||
matched_topic = ""
|
||||
for frame in iter_mcap_frames(mcap_paths, tuple()):
|
||||
if not topic_matches_camera(frame["topic"], target_camera):
|
||||
continue
|
||||
if not matched_topic:
|
||||
matched_topic = str(frame["topic"])
|
||||
selected.append(frame)
|
||||
|
||||
if not selected:
|
||||
raise FileNotFoundError(f"no MCAP frames found for camera selector {target_camera}")
|
||||
|
||||
return selected, matched_topic
|
||||
|
||||
|
||||
def find_video_frame_match(index_payload: Optional[dict[str, Any]], target_frame_id: int) -> tuple[int | None, str | None]:
|
||||
if not index_payload:
|
||||
return None, None
|
||||
|
||||
fields = index_payload.get("fields", {}) or {}
|
||||
index_list = index_payload.get("index", []) or []
|
||||
for field_name in ("frame_id", "cve_frame_id"):
|
||||
if field_name not in fields:
|
||||
continue
|
||||
for frame_idx in range(len(index_list)):
|
||||
info = get_video_frame_info(index_payload, frame_idx)
|
||||
if info is None:
|
||||
continue
|
||||
value = info.get(field_name)
|
||||
try:
|
||||
if int(value) == target_frame_id:
|
||||
return frame_idx, field_name
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
return None, None
|
||||
|
||||
|
||||
def collect_video_window(
|
||||
video_path: Path,
|
||||
target_frame_id: int,
|
||||
window_radius: int,
|
||||
) -> tuple[list[dict[str, Any]], str, int]:
|
||||
index_payload = read_video_frame_index(video_path)
|
||||
matched_frame_idx, matched_field = find_video_frame_match(index_payload, target_frame_id)
|
||||
if matched_frame_idx is None or matched_field is None:
|
||||
raise FileNotFoundError(f"target frame_id {target_frame_id} not found in video index")
|
||||
|
||||
start_idx = max(0, matched_frame_idx - window_radius)
|
||||
end_idx = matched_frame_idx + window_radius
|
||||
|
||||
cap = cv2.VideoCapture(str(video_path))
|
||||
if not cap.isOpened():
|
||||
raise RuntimeError(f"failed to open video file: {video_path}")
|
||||
|
||||
selected: list[dict[str, Any]] = []
|
||||
try:
|
||||
cap.set(cv2.CAP_PROP_POS_FRAMES, start_idx)
|
||||
current_idx = start_idx
|
||||
while current_idx <= end_idx:
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
break
|
||||
frame_info = get_video_frame_info(index_payload, current_idx) if index_payload else None
|
||||
selected.append(
|
||||
{
|
||||
"source_path": video_path,
|
||||
"frame_idx": current_idx,
|
||||
"frame_id": None if frame_info is None else frame_info.get("frame_id"),
|
||||
"cve_frame_id": None if frame_info is None else frame_info.get("cve_frame_id"),
|
||||
"timestamp": "" if frame_info is None else str(frame_info.get("timestamp", "")),
|
||||
"image": frame,
|
||||
}
|
||||
)
|
||||
current_idx += 1
|
||||
finally:
|
||||
cap.release()
|
||||
|
||||
return selected, matched_field, matched_frame_idx
|
||||
|
||||
|
||||
def collect_video_all_frames(video_path: Path) -> tuple[list[dict[str, Any]], int]:
|
||||
index_payload = read_video_frame_index(video_path)
|
||||
cap = cv2.VideoCapture(str(video_path))
|
||||
if not cap.isOpened():
|
||||
raise RuntimeError(f"failed to open video file: {video_path}")
|
||||
|
||||
selected: list[dict[str, Any]] = []
|
||||
current_idx = 0
|
||||
try:
|
||||
while True:
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
break
|
||||
frame_info = get_video_frame_info(index_payload, current_idx) if index_payload else None
|
||||
selected.append(
|
||||
{
|
||||
"source_path": video_path,
|
||||
"frame_idx": current_idx,
|
||||
"frame_id": None if frame_info is None else frame_info.get("frame_id"),
|
||||
"cve_frame_id": None if frame_info is None else frame_info.get("cve_frame_id"),
|
||||
"timestamp": "" if frame_info is None else str(frame_info.get("timestamp", "")),
|
||||
"image": frame,
|
||||
}
|
||||
)
|
||||
current_idx += 1
|
||||
finally:
|
||||
cap.release()
|
||||
|
||||
if not selected:
|
||||
raise FileNotFoundError(f"no readable frames found in video file: {video_path}")
|
||||
return selected, len(selected)
|
||||
|
||||
|
||||
def save_decoded_frames(
|
||||
issue_id: int,
|
||||
output_dir: Path,
|
||||
frames: list[dict[str, Any]],
|
||||
target_frame_id: int,
|
||||
window_radius: int,
|
||||
jpg_quality: int,
|
||||
dry_run: bool,
|
||||
decode_all: bool,
|
||||
) -> int:
|
||||
images_dir = output_dir / ("frames_all" if decode_all else "frames_window")
|
||||
if dry_run:
|
||||
return len(frames)
|
||||
|
||||
ensure_dir(images_dir, dry_run=False)
|
||||
encode_params = [int(cv2.IMWRITE_JPEG_QUALITY), int(jpg_quality)]
|
||||
target_index = None if decode_all else min(window_radius, len(frames) - 1)
|
||||
for index, frame in enumerate(frames):
|
||||
offset = 0 if target_index is None else index - target_index
|
||||
frame_id = frame.get("frame_id")
|
||||
if frame_id is None:
|
||||
frame_id = frame.get("cve_frame_id")
|
||||
frame_id_token = "na" if frame_id is None else str(frame_id)
|
||||
topic = str(frame.get("topic") or "")
|
||||
camera_id_match = re.search(r"camera(\d+)", topic, re.IGNORECASE)
|
||||
if camera_id_match:
|
||||
camera_id_token = f"camera{camera_id_match.group(1)}"
|
||||
else:
|
||||
source_path = Path(str(frame.get("source_path") or ""))
|
||||
camera_id_token = source_path.stem if source_path.stem else "camera"
|
||||
if decode_all:
|
||||
filename = f"{issue_id}_{camera_id_token}_{frame_id_token}.jpg"
|
||||
else:
|
||||
filename = f"{issue_id}_{camera_id_token}_{frame_id_token}.jpg"
|
||||
|
||||
image = frame.get("image")
|
||||
if image is None:
|
||||
image = _h265_payload_to_bgr(frame["payload"])
|
||||
if not cv2.imwrite(str(images_dir / filename), image, encode_params):
|
||||
raise IOError(f"failed to write image: {images_dir / filename}")
|
||||
return len(frames)
|
||||
|
||||
|
||||
def process_case(case: CaseSource, output_root: Path, window_radius: int, jpg_quality: int, dry_run: bool, skip_existing: bool) -> CaseResult:
|
||||
output_dir = output_root / case.relative_case_dir
|
||||
images_dir = output_dir / ("frames_all" if case.decode_all else "frames_window")
|
||||
if skip_existing and images_dir.is_dir():
|
||||
existing_images = [path for path in images_dir.iterdir() if path.is_file()]
|
||||
if existing_images:
|
||||
return CaseResult(
|
||||
issue_id=case.issue_id,
|
||||
issue_name=case.issue_name,
|
||||
case_dir=str(case.case_dir),
|
||||
relative_case_dir=str(case.relative_case_dir),
|
||||
target_camera=case.target_frame.camera,
|
||||
target_frame_id=case.target_frame.frame_id,
|
||||
decode_mode="all" if case.decode_all else "window",
|
||||
source_mode=case.source_mode,
|
||||
source_paths=[str(path) for path in case.source_paths],
|
||||
output_dir=str(output_dir),
|
||||
status="skipped_existing",
|
||||
detail="existing extracted frames found",
|
||||
extracted_count=len(existing_images),
|
||||
)
|
||||
|
||||
try:
|
||||
if case.source_mode == "mcap_stream":
|
||||
try:
|
||||
if case.decode_all:
|
||||
frames, matched_topic = collect_mcap_all_frames(case.source_paths, case.target_frame.camera)
|
||||
matched_field = None
|
||||
else:
|
||||
frames, matched_field, matched_topic = collect_mcap_window(
|
||||
case.source_paths,
|
||||
case.target_frame.camera,
|
||||
case.target_frame.frame_id,
|
||||
window_radius,
|
||||
)
|
||||
extracted_count = save_decoded_frames(
|
||||
case.issue_id, output_dir, frames, case.target_frame.frame_id, window_radius, jpg_quality, dry_run, case.decode_all
|
||||
)
|
||||
detail = (
|
||||
f"decoded {extracted_count} frames from {len(case.source_paths)} mcap file(s)"
|
||||
if case.decode_all
|
||||
else f"decoded {extracted_count} frames from {len(case.source_paths)} mcap file(s)"
|
||||
)
|
||||
return CaseResult(
|
||||
issue_id=case.issue_id,
|
||||
issue_name=case.issue_name,
|
||||
case_dir=str(case.case_dir),
|
||||
relative_case_dir=str(case.relative_case_dir),
|
||||
target_camera=case.target_frame.camera,
|
||||
target_frame_id=case.target_frame.frame_id,
|
||||
decode_mode="all" if case.decode_all else "window",
|
||||
source_mode=case.source_mode,
|
||||
source_paths=[str(path) for path in case.source_paths],
|
||||
output_dir=str(output_dir),
|
||||
status="planned" if dry_run else "decoded_mcap",
|
||||
detail=("would " + detail) if dry_run else detail,
|
||||
matched_field=matched_field,
|
||||
matched_frame_idx=None,
|
||||
matched_topic=matched_topic,
|
||||
extracted_count=extracted_count,
|
||||
)
|
||||
except Exception as mcap_exc:
|
||||
fallback_camera4 = find_camera4_bin(case.case_dir)
|
||||
if fallback_camera4 is None or case.target_frame.camera not in {"camera4", "any"}:
|
||||
raise mcap_exc
|
||||
|
||||
if case.decode_all:
|
||||
frames, extracted_count_all = collect_video_all_frames(fallback_camera4)
|
||||
matched_field = None
|
||||
matched_frame_idx = None
|
||||
extracted_count = save_decoded_frames(
|
||||
case.issue_id, output_dir, frames, case.target_frame.frame_id, window_radius, jpg_quality, dry_run, case.decode_all
|
||||
)
|
||||
else:
|
||||
frames, matched_field, matched_frame_idx = collect_video_window(
|
||||
fallback_camera4,
|
||||
case.target_frame.frame_id,
|
||||
window_radius,
|
||||
)
|
||||
extracted_count = save_decoded_frames(
|
||||
case.issue_id, output_dir, frames, case.target_frame.frame_id, window_radius, jpg_quality, dry_run, case.decode_all
|
||||
)
|
||||
detail = (
|
||||
f"decoded {extracted_count} frames from camera4.bin fallback after mcap lookup failed: "
|
||||
f"{type(mcap_exc).__name__}: {mcap_exc}"
|
||||
)
|
||||
return CaseResult(
|
||||
issue_id=case.issue_id,
|
||||
issue_name=case.issue_name,
|
||||
case_dir=str(case.case_dir),
|
||||
relative_case_dir=str(case.relative_case_dir),
|
||||
target_camera=case.target_frame.camera,
|
||||
target_frame_id=case.target_frame.frame_id,
|
||||
decode_mode="all" if case.decode_all else "window",
|
||||
source_mode="camera4_bin_fallback",
|
||||
source_paths=[str(path) for path in case.source_paths] + [str(fallback_camera4)],
|
||||
output_dir=str(output_dir),
|
||||
status="planned" if dry_run else "decoded_camera4_bin_fallback",
|
||||
detail=("would " + detail) if dry_run else detail,
|
||||
matched_field=matched_field,
|
||||
matched_frame_idx=matched_frame_idx,
|
||||
matched_topic=None,
|
||||
extracted_count=extracted_count,
|
||||
)
|
||||
|
||||
if case.source_mode == "camera4_bin":
|
||||
if case.decode_all:
|
||||
frames, _ = collect_video_all_frames(case.source_paths[0])
|
||||
matched_field, matched_frame_idx = None, None
|
||||
else:
|
||||
frames, matched_field, matched_frame_idx = collect_video_window(case.source_paths[0], case.target_frame.frame_id, window_radius)
|
||||
extracted_count = save_decoded_frames(
|
||||
case.issue_id, output_dir, frames, case.target_frame.frame_id, window_radius, jpg_quality, dry_run, case.decode_all
|
||||
)
|
||||
detail = f"decoded {extracted_count} frames from camera4.bin"
|
||||
return CaseResult(
|
||||
issue_id=case.issue_id,
|
||||
issue_name=case.issue_name,
|
||||
case_dir=str(case.case_dir),
|
||||
relative_case_dir=str(case.relative_case_dir),
|
||||
target_camera=case.target_frame.camera,
|
||||
target_frame_id=case.target_frame.frame_id,
|
||||
decode_mode="all" if case.decode_all else "window",
|
||||
source_mode=case.source_mode,
|
||||
source_paths=[str(path) for path in case.source_paths],
|
||||
output_dir=str(output_dir),
|
||||
status="planned" if dry_run else "decoded_camera4_bin",
|
||||
detail=("would " + detail) if dry_run else detail,
|
||||
matched_field=matched_field,
|
||||
matched_frame_idx=matched_frame_idx,
|
||||
matched_topic=None,
|
||||
extracted_count=extracted_count,
|
||||
)
|
||||
|
||||
return CaseResult(
|
||||
issue_id=case.issue_id,
|
||||
issue_name=case.issue_name,
|
||||
case_dir=str(case.case_dir),
|
||||
relative_case_dir=str(case.relative_case_dir),
|
||||
target_camera=case.target_frame.camera,
|
||||
target_frame_id=case.target_frame.frame_id,
|
||||
decode_mode="all" if case.decode_all else "window",
|
||||
source_mode=case.source_mode,
|
||||
source_paths=[str(path) for path in case.source_paths],
|
||||
output_dir=str(output_dir),
|
||||
status="skipped_no_source",
|
||||
detail=f"unsupported source mode: {case.source_mode}",
|
||||
)
|
||||
except Exception as exc:
|
||||
return CaseResult(
|
||||
issue_id=case.issue_id,
|
||||
issue_name=case.issue_name,
|
||||
case_dir=str(case.case_dir),
|
||||
relative_case_dir=str(case.relative_case_dir),
|
||||
target_camera=case.target_frame.camera,
|
||||
target_frame_id=case.target_frame.frame_id,
|
||||
decode_mode="all" if case.decode_all else "window",
|
||||
source_mode=case.source_mode,
|
||||
source_paths=[str(path) for path in case.source_paths],
|
||||
output_dir=str(output_dir),
|
||||
status="failed",
|
||||
detail=f"{type(exc).__name__}: {exc}",
|
||||
)
|
||||
|
||||
|
||||
def build_manifest(args: argparse.Namespace, input_json: Path, download_root: Path, output_root: Path, discovered: list[CaseSource], results: list[CaseResult]) -> dict[str, Any]:
|
||||
summary = Counter(result.status for result in results)
|
||||
return {
|
||||
"generated_at": datetime.now().astimezone().isoformat(timespec="seconds"),
|
||||
"input_json": str(input_json),
|
||||
"download_root": str(download_root),
|
||||
"output_root": str(output_root),
|
||||
"issue_filter": args.issue_ids or [],
|
||||
"decode_all_issue_filter": args.decode_all_issue_ids or [],
|
||||
"window_radius": args.window_radius,
|
||||
"window_size": args.window_radius * 2 + 1,
|
||||
"jpg_quality": args.jpg_quality,
|
||||
"dry_run": args.dry_run,
|
||||
"skip_existing": args.skip_existing,
|
||||
"total_discovered_cases": len(discovered),
|
||||
"summary": dict(summary),
|
||||
"cases": [result.to_dict() for result in results],
|
||||
}
|
||||
|
||||
|
||||
def print_summary(manifest: dict[str, Any]) -> None:
|
||||
print(f"input_json: {manifest['input_json']}")
|
||||
print(f"download_root: {manifest['download_root']}")
|
||||
print(f"output_root: {manifest['output_root']}")
|
||||
print(f"window_size: {manifest['window_size']}")
|
||||
print(f"dry_run: {manifest['dry_run']}")
|
||||
print(f"total_discovered_cases: {manifest['total_discovered_cases']}")
|
||||
for status, count in sorted(manifest["summary"].items()):
|
||||
print(f"{status}: {count}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
input_json = Path(args.input_json).resolve()
|
||||
download_root = Path(args.download_root).resolve()
|
||||
output_root = Path(args.output_root).resolve()
|
||||
manifest_path = (
|
||||
Path(args.manifest_path).resolve()
|
||||
if args.manifest_path
|
||||
else output_root / "decode_manifest.json"
|
||||
)
|
||||
|
||||
items = load_issue_items(input_json)
|
||||
issue_filter = set(args.issue_ids) if args.issue_ids else None
|
||||
decode_all_issue_filter = set(args.decode_all_issue_ids or [])
|
||||
discovered, skipped = discover_case_sources(items, download_root, issue_filter, decode_all_issue_filter)
|
||||
results = list(skipped)
|
||||
for case in discovered:
|
||||
results.append(process_case(case, output_root, args.window_radius, args.jpg_quality, args.dry_run, args.skip_existing))
|
||||
|
||||
manifest = build_manifest(args, input_json, download_root, output_root, discovered, results)
|
||||
if not args.dry_run:
|
||||
ensure_dir(manifest_path.parent, dry_run=False)
|
||||
manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
print_summary(manifest)
|
||||
if args.dry_run:
|
||||
print(f"manifest (not written in dry-run): {manifest_path}")
|
||||
else:
|
||||
print(f"manifest: {manifest_path}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
45
tools/feishu_project/decode_issue_frame_window.sh
Executable file
45
tools/feishu_project/decode_issue_frame_window.sh
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
PROJECT_ROOT=$(cd "${SCRIPT_DIR}/../.." && pwd)
|
||||
|
||||
INPUT_JSON=${INPUT_JSON:-"${PROJECT_ROOT}/tools/feishu_project/dongying_d4q2_zhibao_issue_list.json"}
|
||||
DOWNLOAD_ROOT=${DOWNLOAD_ROOT:-/data1/dongying/Mono3d/D4Q2/feishu_project/downloaded_issue_data}
|
||||
OUTPUT_ROOT=${OUTPUT_ROOT:-/data1/dongying/Mono3d/D4Q2/feishu_project/decoded_issue_frame_windows_v2}
|
||||
MANIFEST_PATH=${MANIFEST_PATH:-"${OUTPUT_ROOT}/decode_manifest.json"}
|
||||
PYTHON_BIN=${PYTHON_BIN:-/deeplearning_team/ydong/dongying/miniconda/envs/dev/bin/python}
|
||||
WINDOW_RADIUS=${WINDOW_RADIUS:-30}
|
||||
JPG_QUALITY=${JPG_QUALITY:-95}
|
||||
DRY_RUN=${DRY_RUN:-0}
|
||||
SKIP_EXISTING=${SKIP_EXISTING:-0}
|
||||
DECODE_ALL_ISSUE_IDS=${DECODE_ALL_ISSUE_IDS:-}
|
||||
|
||||
CMD=(
|
||||
"${PYTHON_BIN}" "${PROJECT_ROOT}/tools/feishu_project/decode_issue_frame_window.py"
|
||||
--input-json "${INPUT_JSON}"
|
||||
--download-root "${DOWNLOAD_ROOT}"
|
||||
--output-root "${OUTPUT_ROOT}"
|
||||
--manifest-path "${MANIFEST_PATH}"
|
||||
--window-radius "${WINDOW_RADIUS}"
|
||||
--jpg-quality "${JPG_QUALITY}"
|
||||
)
|
||||
|
||||
if [[ "${DRY_RUN}" == "1" ]]; then
|
||||
CMD+=(--dry-run)
|
||||
fi
|
||||
|
||||
if [[ "${SKIP_EXISTING}" == "1" ]]; then
|
||||
CMD+=(--skip-existing)
|
||||
fi
|
||||
|
||||
if [[ -n "${DECODE_ALL_ISSUE_IDS}" ]]; then
|
||||
# shellcheck disable=SC2206
|
||||
ISSUE_ARR=(${DECODE_ALL_ISSUE_IDS})
|
||||
for issue_id in "${ISSUE_ARR[@]}"; do
|
||||
CMD+=(--decode-all-issue-id "${issue_id}")
|
||||
done
|
||||
fi
|
||||
|
||||
CMD+=("$@")
|
||||
"${CMD[@]}"
|
||||
1111
tools/feishu_project/dongying_g1q3_issue_list.json
Executable file
1111
tools/feishu_project/dongying_g1q3_issue_list.json
Executable file
File diff suppressed because it is too large
Load Diff
805
tools/feishu_project/download_issue_data.py
Executable file
805
tools/feishu_project/download_issue_data.py
Executable file
@@ -0,0 +1,805 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Download issue data referenced by the Feishu issue export JSON."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
FILE = Path(__file__).resolve()
|
||||
ROOT = FILE.parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.append(str(ROOT))
|
||||
|
||||
from tools.feishu_project.case_calib_recovery import recover_camera4_json
|
||||
|
||||
|
||||
PDCL_REF_RE = re.compile(r"ADAS_[^:/\\\s]+::[^/\\\s]*")
|
||||
MDI_RAW_REF_ARG_RE = re.compile(r"(?:^|\s)mdi\s+raw\b.*?(?:^|\s)-r\s+([^\s]+)")
|
||||
STANDARD_PATH_SPLIT_RE = re.compile(r"[,,\n;;]+")
|
||||
SHARED_CALIB_REL = Path("test_data") / "calibs" / "camera4.json"
|
||||
PLACEHOLDER_TEXTS = {"待填", "待补充", "none", "null", "待提供"}
|
||||
NETWORK_SHARE_PREFIX_MAPPINGS = (
|
||||
("//hfs.minieye.tech/project-D4Q2", "/mnt/D4Q2"),
|
||||
("//192.168.2.122/project-D4Q2", "/mnt/D4Q2"),
|
||||
("//hfs.minieye.tech/project-G1M3", "/mnt/G1M3"),
|
||||
("//192.168.2.122/project-G1M3", "/mnt/G1M3"),
|
||||
("//hfs.minieye.tech/G1M3", "/mnt/G1M3"),
|
||||
("//192.168.2.122/G1M3", "/mnt/G1M3"),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ActionResult:
|
||||
issue_id: int
|
||||
issue_name: str
|
||||
source_field: str
|
||||
source_kind: str
|
||||
raw_value: str | None
|
||||
normalized_ref: str | None
|
||||
output_dir: str
|
||||
status: str
|
||||
detail: str
|
||||
resolved_source_path: str | None = None
|
||||
command: list[str] | None = None
|
||||
candidate_paths: list[str] | None = None
|
||||
selected_subpath: str | None = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"issue_id": self.issue_id,
|
||||
"issue_name": self.issue_name,
|
||||
"source_field": self.source_field,
|
||||
"source_kind": self.source_kind,
|
||||
"raw_value": self.raw_value,
|
||||
"normalized_ref": self.normalized_ref,
|
||||
"output_dir": self.output_dir,
|
||||
"status": self.status,
|
||||
"detail": self.detail,
|
||||
"resolved_source_path": self.resolved_source_path,
|
||||
"command": self.command,
|
||||
"candidate_paths": self.candidate_paths,
|
||||
"selected_subpath": self.selected_subpath,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PDCLDownloadRequest:
|
||||
normalized_ref: str
|
||||
selected_subpath: str | None = None
|
||||
raw_token: str | None = None
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Download or stage issue data from a Feishu issue export JSON."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--input-json",
|
||||
default="tools/feishu_project/dongying_g1q3_issue_list.json",
|
||||
help="Path to the issue export JSON.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-root",
|
||||
required=True,
|
||||
help="Directory where downloaded or copied data should be stored.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--manifest-path",
|
||||
default=None,
|
||||
help="Optional explicit path for the execution manifest JSON.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--issue-id",
|
||||
action="append",
|
||||
dest="issue_ids",
|
||||
type=int,
|
||||
help="Optional issue id filter. Can be repeated.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Plan actions without running mdi or copying files.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-mdi",
|
||||
action="store_true",
|
||||
help="Skip PDCL/MDI downloads and only process standard paths.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-copy",
|
||||
action="store_true",
|
||||
help="Skip standard-path copies and only process PDCL/MDI downloads.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--only-redownload-affected-cases",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Only re-copy standard-path cases affected by the historical sigmastar.1/camera4.bin "
|
||||
"copy bug. This mode skips PDCL/MDI downloads and replaces stale copied targets."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-calib-recovery",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Skip recovering camera4.json from camera_config_folder.bin or mcap attachments "
|
||||
"after standard-path copies."
|
||||
),
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def load_issue_items(path: Path) -> list[dict]:
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
return payload["items"]
|
||||
|
||||
|
||||
def ensure_dir(path: Path, dry_run: bool) -> None:
|
||||
if dry_run:
|
||||
return
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def log_progress(message: str) -> None:
|
||||
timestamp = datetime.now().astimezone().strftime("%Y-%m-%d %H:%M:%S")
|
||||
print(f"[download_issue_data {timestamp}] {message}", flush=True)
|
||||
|
||||
|
||||
def compact_text(value: object, max_len: int = 96) -> str:
|
||||
text = "" if value is None else str(value).strip()
|
||||
text = re.sub(r"\s+", " ", text)
|
||||
if len(text) <= max_len:
|
||||
return text
|
||||
return f"{text[: max_len - 3]}..."
|
||||
|
||||
|
||||
def summarize_issue_results(results: list[ActionResult]) -> str:
|
||||
if not results:
|
||||
return "no actions"
|
||||
summary: dict[str, int] = {}
|
||||
for result in results:
|
||||
summary[result.status] = summary.get(result.status, 0) + 1
|
||||
return ", ".join(f"{status}={summary[status]}" for status in sorted(summary))
|
||||
|
||||
|
||||
def normalize_issue_dirname(issue_id: int) -> str:
|
||||
return f"issue_{issue_id}"
|
||||
|
||||
|
||||
def iter_issue_fields(item: dict) -> Iterable[tuple[str, object]]:
|
||||
yield "问题数据地址", item.get("问题数据地址")
|
||||
yield "问题数据地址_PDCL", item.get("问题数据地址_PDCL")
|
||||
|
||||
|
||||
def _normalize_pdcl_selected_subpath(raw_subpath: str) -> str | None:
|
||||
cleaned = raw_subpath.strip().strip("/")
|
||||
if not cleaned:
|
||||
return None
|
||||
candidate = Path(cleaned)
|
||||
if candidate.is_absolute():
|
||||
return None
|
||||
if any(part in {"", ".", ".."} for part in candidate.parts):
|
||||
return None
|
||||
return str(candidate)
|
||||
|
||||
|
||||
def _build_pdcl_request_from_token(token: str) -> PDCLDownloadRequest | None:
|
||||
stripped = token.strip().strip("\"'`")
|
||||
match = PDCL_REF_RE.match(stripped)
|
||||
if match is None:
|
||||
return None
|
||||
normalized_ref = match.group(0)
|
||||
suffix = stripped[match.end():]
|
||||
selected_subpath = None
|
||||
if suffix.startswith("/"):
|
||||
raw_subpath = suffix[1:]
|
||||
if raw_subpath and not raw_subpath.startswith("ADAS_"):
|
||||
selected_subpath = _normalize_pdcl_selected_subpath(raw_subpath)
|
||||
return PDCLDownloadRequest(
|
||||
normalized_ref=normalized_ref,
|
||||
selected_subpath=selected_subpath,
|
||||
raw_token=stripped,
|
||||
)
|
||||
|
||||
|
||||
def extract_pdcl_requests(raw_value: object) -> list[PDCLDownloadRequest]:
|
||||
if raw_value is None:
|
||||
return []
|
||||
text = str(raw_value).strip()
|
||||
if not text:
|
||||
return []
|
||||
requests: list[PDCLDownloadRequest] = []
|
||||
|
||||
for segment in (part.strip() for part in STANDARD_PATH_SPLIT_RE.split(text)):
|
||||
if not segment:
|
||||
continue
|
||||
|
||||
mdi_match = MDI_RAW_REF_ARG_RE.search(segment)
|
||||
if mdi_match is not None:
|
||||
request = _build_pdcl_request_from_token(mdi_match.group(1))
|
||||
if request is not None:
|
||||
requests.append(request)
|
||||
continue
|
||||
|
||||
search_pos = 0
|
||||
while True:
|
||||
match = PDCL_REF_RE.search(segment, search_pos)
|
||||
if match is None:
|
||||
break
|
||||
normalized_ref = match.group(0)
|
||||
suffix = segment[match.end():]
|
||||
selected_subpath = None
|
||||
if suffix.startswith("/") and not suffix[1:].startswith("ADAS_"):
|
||||
selected_subpath = _normalize_pdcl_selected_subpath(suffix[1:])
|
||||
requests.append(
|
||||
PDCLDownloadRequest(
|
||||
normalized_ref=normalized_ref,
|
||||
selected_subpath=selected_subpath,
|
||||
raw_token=segment,
|
||||
)
|
||||
)
|
||||
break
|
||||
|
||||
requests.append(
|
||||
PDCLDownloadRequest(
|
||||
normalized_ref=normalized_ref,
|
||||
selected_subpath=None,
|
||||
raw_token=normalized_ref,
|
||||
)
|
||||
)
|
||||
search_pos = match.end()
|
||||
|
||||
deduped: list[PDCLDownloadRequest] = []
|
||||
seen = set()
|
||||
for request in requests:
|
||||
key = (request.normalized_ref, request.selected_subpath)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
deduped.append(request)
|
||||
return deduped
|
||||
|
||||
|
||||
def extract_standard_paths(raw_value: object) -> list[str]:
|
||||
if raw_value is None:
|
||||
return []
|
||||
text = str(raw_value).strip()
|
||||
if not text:
|
||||
return []
|
||||
if text.lower() in PLACEHOLDER_TEXTS:
|
||||
return []
|
||||
if extract_pdcl_requests(text):
|
||||
return []
|
||||
if "/" not in text and "\\" not in text:
|
||||
return []
|
||||
parts = [part.strip() for part in STANDARD_PATH_SPLIT_RE.split(text)]
|
||||
return [part for part in parts if part]
|
||||
|
||||
|
||||
def normalize_standard_source_path(path: Path) -> Path:
|
||||
normalized = path
|
||||
if normalized.name == "camera4.bin" and normalized.parent.name == "sigmastar.1":
|
||||
return normalized.parent.parent
|
||||
if normalized.name == "sigmastar.1":
|
||||
return normalized.parent
|
||||
return normalized
|
||||
|
||||
|
||||
def is_affected_standard_path(raw_path: str) -> bool:
|
||||
raw_path_obj = Path(raw_path.strip())
|
||||
return normalize_standard_source_path(raw_path_obj) != raw_path_obj
|
||||
|
||||
|
||||
def normalize_share_path_separators(path_str: str) -> str:
|
||||
normalized = path_str.strip().replace("\\", "/")
|
||||
normalized = re.sub(r"/{3,}", "//", normalized)
|
||||
return normalized
|
||||
|
||||
|
||||
def rewrite_network_share_path(path_str: str) -> str | None:
|
||||
normalized = normalize_share_path_separators(path_str)
|
||||
for prefix_src, prefix_dst in NETWORK_SHARE_PREFIX_MAPPINGS:
|
||||
if normalized.startswith(prefix_src):
|
||||
return f"{prefix_dst}{normalized[len(prefix_src):]}"
|
||||
return None
|
||||
|
||||
|
||||
def build_path_candidates(raw_path: str) -> list[Path]:
|
||||
candidates: list[str] = [raw_path]
|
||||
|
||||
normalized_share = normalize_share_path_separators(raw_path)
|
||||
if normalized_share != raw_path:
|
||||
candidates.append(normalized_share)
|
||||
|
||||
network_share_rewritten = rewrite_network_share_path(raw_path)
|
||||
if network_share_rewritten:
|
||||
candidates.append(network_share_rewritten)
|
||||
|
||||
for needle in ("hfs/project-G1M3", "project-G1M3"):
|
||||
for candidate in list(candidates):
|
||||
if needle in candidate:
|
||||
candidates.append(candidate.replace(needle, "G1M3"))
|
||||
|
||||
normalized_candidates = [normalize_standard_source_path(Path(candidate)) for candidate in candidates]
|
||||
unique_candidates = list(OrderedDict.fromkeys(str(candidate) for candidate in normalized_candidates))
|
||||
return [Path(candidate) for candidate in unique_candidates]
|
||||
|
||||
|
||||
def resolve_existing_path(raw_path: str) -> tuple[Path | None, list[Path]]:
|
||||
candidates = build_path_candidates(raw_path)
|
||||
for candidate in candidates:
|
||||
if candidate.exists():
|
||||
return candidate, candidates
|
||||
return None, candidates
|
||||
|
||||
|
||||
def remove_existing_target(path: Path) -> None:
|
||||
if path.is_dir():
|
||||
shutil.rmtree(path)
|
||||
else:
|
||||
path.unlink()
|
||||
|
||||
|
||||
def copy_source_path(
|
||||
source_path: Path,
|
||||
output_dir: Path,
|
||||
dry_run: bool,
|
||||
replace_existing: bool = False,
|
||||
legacy_target_names: Iterable[str] | None = None,
|
||||
) -> tuple[str, str]:
|
||||
target = output_dir / source_path.name
|
||||
cleanup_targets: list[Path] = [target]
|
||||
if legacy_target_names:
|
||||
for target_name in legacy_target_names:
|
||||
cleanup_targets.append(output_dir / target_name)
|
||||
|
||||
unique_cleanup_targets = list(OrderedDict.fromkeys(str(path) for path in cleanup_targets))
|
||||
cleanup_paths = [Path(path) for path in unique_cleanup_targets]
|
||||
existing_targets = [path for path in cleanup_paths if path.exists()]
|
||||
|
||||
if existing_targets and not replace_existing:
|
||||
return "exists", f"target already exists: {target}"
|
||||
|
||||
if dry_run:
|
||||
if existing_targets and replace_existing:
|
||||
existing_str = ", ".join(str(path) for path in existing_targets)
|
||||
return "planned_redownload", f"would replace {existing_str} with {source_path} -> {target}"
|
||||
return "planned", f"would copy {source_path} -> {target}"
|
||||
|
||||
ensure_dir(output_dir, dry_run=False)
|
||||
if replace_existing:
|
||||
for existing_target in existing_targets:
|
||||
remove_existing_target(existing_target)
|
||||
|
||||
if source_path.is_dir():
|
||||
shutil.copytree(source_path, target)
|
||||
else:
|
||||
shutil.copy2(source_path, target)
|
||||
if existing_targets and replace_existing:
|
||||
replaced = ", ".join(str(path) for path in existing_targets)
|
||||
return "redownloaded", f"replaced {replaced} with {target}"
|
||||
return "copied", f"copied to {target}"
|
||||
|
||||
|
||||
def build_copied_target_root(output_dir: Path, source_path: Path) -> Path:
|
||||
return output_dir / source_path.name
|
||||
|
||||
|
||||
def find_shared_test_data_dir(source_path: Path, max_parent_levels: int = 4) -> Path | None:
|
||||
if (source_path / SHARED_CALIB_REL).is_file():
|
||||
return None
|
||||
|
||||
current = source_path.parent
|
||||
for _ in range(max_parent_levels):
|
||||
candidate = current / "test_data"
|
||||
if (candidate / "calibs" / "camera4.json").is_file():
|
||||
return candidate
|
||||
if current.parent == current:
|
||||
break
|
||||
current = current.parent
|
||||
return None
|
||||
|
||||
|
||||
def sync_shared_test_data(
|
||||
source_path: Path,
|
||||
target_root: Path,
|
||||
dry_run: bool,
|
||||
) -> tuple[str | None, str | None, str | None]:
|
||||
shared_test_data_dir = find_shared_test_data_dir(source_path)
|
||||
if shared_test_data_dir is None:
|
||||
return None, None, None
|
||||
|
||||
target_shared_test_data_dir = target_root / "test_data"
|
||||
target_shared_calib = target_shared_test_data_dir / "calibs" / "camera4.json"
|
||||
if target_shared_calib.is_file():
|
||||
return None, None, None
|
||||
|
||||
if dry_run:
|
||||
return (
|
||||
"planned_shared_calib_sync",
|
||||
f"would copy shared test_data {shared_test_data_dir} -> {target_shared_test_data_dir}",
|
||||
str(shared_test_data_dir),
|
||||
)
|
||||
|
||||
shutil.copytree(shared_test_data_dir, target_shared_test_data_dir, dirs_exist_ok=True)
|
||||
return (
|
||||
"synced_shared_calib",
|
||||
f"copied shared test_data {shared_test_data_dir} -> {target_shared_test_data_dir}",
|
||||
str(shared_test_data_dir),
|
||||
)
|
||||
|
||||
|
||||
def recover_target_root_calib(
|
||||
source_root: Path,
|
||||
target_root: Path,
|
||||
dry_run: bool,
|
||||
) -> tuple[str, str, str | None]:
|
||||
recovery = recover_camera4_json(
|
||||
source_root=source_root,
|
||||
target_root=target_root,
|
||||
dry_run=dry_run,
|
||||
)
|
||||
return recovery.status, recovery.detail, None if recovery.source_path is None else str(recovery.source_path)
|
||||
|
||||
|
||||
def _expected_pdcl_root_dir(output_dir: Path, ref: str) -> Path:
|
||||
if "::" not in ref:
|
||||
raise ValueError(f"Unexpected PDCL ref without '::': {ref}")
|
||||
return output_dir / ref.split("::", 1)[1]
|
||||
|
||||
|
||||
def _prune_pdcl_download_to_selected_subpath(
|
||||
root_dir: Path,
|
||||
selected_subpath: str,
|
||||
dry_run: bool,
|
||||
) -> tuple[str, str]:
|
||||
selected_rel = Path(selected_subpath)
|
||||
selected_path = root_dir / selected_rel
|
||||
if dry_run:
|
||||
return (
|
||||
"planned_selected_subpath",
|
||||
f"would keep {selected_path} and shared test_data under {root_dir}",
|
||||
)
|
||||
|
||||
if not selected_path.exists():
|
||||
return (
|
||||
"failed_selected_subpath_missing",
|
||||
f"selected subpath not found after mdi download: {selected_path}",
|
||||
)
|
||||
|
||||
keep_names = {selected_rel.parts[0], "test_data"}
|
||||
removed_children: list[str] = []
|
||||
|
||||
for child in root_dir.iterdir():
|
||||
if child.name in keep_names:
|
||||
continue
|
||||
removed_children.append(child.name)
|
||||
remove_existing_target(child)
|
||||
|
||||
detail = f"kept selected subpath {selected_path}"
|
||||
if removed_children:
|
||||
detail += f"; removed siblings: {', '.join(sorted(removed_children))}"
|
||||
return "downloaded_selected_subpath", detail
|
||||
|
||||
|
||||
def run_mdi_download(request: PDCLDownloadRequest, output_dir: Path, dry_run: bool) -> tuple[str, str, list[str]]:
|
||||
command = ["mdi", "raw", "-r", request.normalized_ref, "-s", str(output_dir)]
|
||||
if dry_run:
|
||||
if request.selected_subpath:
|
||||
root_dir = _expected_pdcl_root_dir(output_dir, request.normalized_ref)
|
||||
status, detail = _prune_pdcl_download_to_selected_subpath(root_dir, request.selected_subpath, dry_run=True)
|
||||
return status, f"would run {' '.join(command)}; {detail}", command
|
||||
return "planned", f"would run {' '.join(command)}", command
|
||||
|
||||
ensure_dir(output_dir, dry_run=False)
|
||||
completed = subprocess.run(
|
||||
command,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
if completed.returncode == 0:
|
||||
base_detail = completed.stdout.strip() or "mdi raw completed"
|
||||
if request.selected_subpath:
|
||||
root_dir = _expected_pdcl_root_dir(output_dir, request.normalized_ref)
|
||||
status, detail = _prune_pdcl_download_to_selected_subpath(root_dir, request.selected_subpath, dry_run=False)
|
||||
return status, f"{base_detail}\n{detail}", command
|
||||
return "downloaded", base_detail, command
|
||||
|
||||
detail = completed.stderr.strip() or completed.stdout.strip() or "mdi raw failed"
|
||||
return "failed", detail, command
|
||||
|
||||
|
||||
def process_issue(
|
||||
item: dict,
|
||||
output_root: Path,
|
||||
dry_run: bool,
|
||||
skip_mdi: bool,
|
||||
skip_copy: bool,
|
||||
only_redownload_affected_cases: bool,
|
||||
skip_calib_recovery: bool,
|
||||
) -> list[ActionResult]:
|
||||
issue_id = int(item["id"])
|
||||
issue_name = str(item["name"])
|
||||
issue_dir = output_root / normalize_issue_dirname(issue_id)
|
||||
|
||||
results: list[ActionResult] = []
|
||||
seen_pdcl_requests: set[tuple[str, str | None]] = set()
|
||||
seen_paths: set[str] = set()
|
||||
pdcl_index = 0
|
||||
path_index = 0
|
||||
|
||||
for field_name, raw_value in iter_issue_fields(item):
|
||||
if not skip_mdi and not only_redownload_affected_cases:
|
||||
for request in extract_pdcl_requests(raw_value):
|
||||
request_key = (request.normalized_ref, request.selected_subpath)
|
||||
if request_key in seen_pdcl_requests:
|
||||
results.append(
|
||||
ActionResult(
|
||||
issue_id=issue_id,
|
||||
issue_name=issue_name,
|
||||
source_field=field_name,
|
||||
source_kind="pdcl_mdi_download",
|
||||
raw_value=None if raw_value is None else str(raw_value),
|
||||
normalized_ref=request.normalized_ref,
|
||||
output_dir=str(issue_dir),
|
||||
status="skipped_duplicate",
|
||||
detail=(
|
||||
f"duplicate PDCL ref: {request.normalized_ref}"
|
||||
if request.selected_subpath is None
|
||||
else f"duplicate PDCL ref+subpath: {request.normalized_ref} / {request.selected_subpath}"
|
||||
),
|
||||
selected_subpath=request.selected_subpath,
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
seen_pdcl_requests.add(request_key)
|
||||
pdcl_index += 1
|
||||
download_dir = issue_dir / f"pdcl_{pdcl_index:02d}"
|
||||
request_desc = request.normalized_ref
|
||||
if request.selected_subpath:
|
||||
request_desc += f"/{request.selected_subpath}"
|
||||
log_progress(
|
||||
f"issue_{issue_id} [download] pdcl_{pdcl_index:02d}: {compact_text(request_desc)}"
|
||||
)
|
||||
status, detail, command = run_mdi_download(request, download_dir, dry_run=dry_run)
|
||||
results.append(
|
||||
ActionResult(
|
||||
issue_id=issue_id,
|
||||
issue_name=issue_name,
|
||||
source_field=field_name,
|
||||
source_kind="pdcl_mdi_download",
|
||||
raw_value=None if raw_value is None else str(raw_value),
|
||||
normalized_ref=request.normalized_ref,
|
||||
output_dir=str(download_dir),
|
||||
status=status,
|
||||
detail=detail,
|
||||
command=command,
|
||||
selected_subpath=request.selected_subpath,
|
||||
)
|
||||
)
|
||||
|
||||
if skip_copy:
|
||||
continue
|
||||
|
||||
for raw_path in extract_standard_paths(raw_value):
|
||||
if raw_path in seen_paths:
|
||||
results.append(
|
||||
ActionResult(
|
||||
issue_id=issue_id,
|
||||
issue_name=issue_name,
|
||||
source_field=field_name,
|
||||
source_kind="standard_path",
|
||||
raw_value=raw_path,
|
||||
normalized_ref=None,
|
||||
output_dir=str(issue_dir),
|
||||
status="skipped_duplicate",
|
||||
detail=f"duplicate standard path: {raw_path}",
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
seen_paths.add(raw_path)
|
||||
path_index += 1
|
||||
copy_dir = issue_dir / f"path_{path_index:02d}"
|
||||
affected_standard_path = is_affected_standard_path(raw_path)
|
||||
if only_redownload_affected_cases and not affected_standard_path:
|
||||
continue
|
||||
|
||||
log_progress(
|
||||
f"issue_{issue_id} [download] path_{path_index:02d}: {compact_text(raw_path)}"
|
||||
)
|
||||
resolved_source_path, candidates = resolve_existing_path(raw_path)
|
||||
if resolved_source_path is None:
|
||||
results.append(
|
||||
ActionResult(
|
||||
issue_id=issue_id,
|
||||
issue_name=issue_name,
|
||||
source_field=field_name,
|
||||
source_kind="standard_path",
|
||||
raw_value=raw_path,
|
||||
normalized_ref=None,
|
||||
output_dir=str(copy_dir),
|
||||
status="skipped_missing",
|
||||
detail="source path not found after rewrite attempts",
|
||||
candidate_paths=[str(candidate) for candidate in candidates],
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
legacy_target_names = []
|
||||
if affected_standard_path:
|
||||
legacy_target_names.append(Path(raw_path.strip()).name)
|
||||
|
||||
status, detail = copy_source_path(
|
||||
resolved_source_path,
|
||||
copy_dir,
|
||||
dry_run=dry_run,
|
||||
replace_existing=only_redownload_affected_cases and affected_standard_path,
|
||||
legacy_target_names=legacy_target_names,
|
||||
)
|
||||
target_root = build_copied_target_root(copy_dir, resolved_source_path)
|
||||
results.append(
|
||||
ActionResult(
|
||||
issue_id=issue_id,
|
||||
issue_name=issue_name,
|
||||
source_field=field_name,
|
||||
source_kind="standard_path",
|
||||
raw_value=raw_path,
|
||||
normalized_ref=None,
|
||||
output_dir=str(copy_dir),
|
||||
status=status,
|
||||
detail=detail,
|
||||
resolved_source_path=str(resolved_source_path),
|
||||
candidate_paths=[str(candidate) for candidate in candidates],
|
||||
)
|
||||
)
|
||||
|
||||
sync_status, sync_detail, shared_source_dir = sync_shared_test_data(
|
||||
resolved_source_path,
|
||||
target_root,
|
||||
dry_run=dry_run,
|
||||
)
|
||||
if sync_status is not None:
|
||||
results.append(
|
||||
ActionResult(
|
||||
issue_id=issue_id,
|
||||
issue_name=issue_name,
|
||||
source_field=field_name,
|
||||
source_kind="shared_test_data",
|
||||
raw_value=raw_path,
|
||||
normalized_ref=None,
|
||||
output_dir=str(target_root / "test_data"),
|
||||
status=sync_status,
|
||||
detail=sync_detail or "",
|
||||
resolved_source_path=shared_source_dir,
|
||||
candidate_paths=[str(candidate) for candidate in candidates],
|
||||
)
|
||||
)
|
||||
|
||||
if not skip_calib_recovery:
|
||||
calib_status, calib_detail, calib_source_path = recover_target_root_calib(
|
||||
source_root=resolved_source_path,
|
||||
target_root=target_root,
|
||||
dry_run=dry_run,
|
||||
)
|
||||
results.append(
|
||||
ActionResult(
|
||||
issue_id=issue_id,
|
||||
issue_name=issue_name,
|
||||
source_field=field_name,
|
||||
source_kind="case_calib_recovery",
|
||||
raw_value=raw_path,
|
||||
normalized_ref=None,
|
||||
output_dir=str(target_root / "test_data" / "calibs"),
|
||||
status=calib_status,
|
||||
detail=calib_detail,
|
||||
resolved_source_path=calib_source_path,
|
||||
candidate_paths=[str(candidate) for candidate in candidates],
|
||||
)
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def build_manifest(
|
||||
args: argparse.Namespace,
|
||||
input_json: Path,
|
||||
output_root: Path,
|
||||
action_results: list[ActionResult],
|
||||
) -> dict:
|
||||
summary: dict[str, int] = {}
|
||||
for action in action_results:
|
||||
summary[action.status] = summary.get(action.status, 0) + 1
|
||||
|
||||
return {
|
||||
"generated_at": datetime.now().astimezone().isoformat(timespec="seconds"),
|
||||
"input_json": str(input_json),
|
||||
"output_root": str(output_root),
|
||||
"dry_run": args.dry_run,
|
||||
"skip_mdi": args.skip_mdi,
|
||||
"skip_copy": args.skip_copy,
|
||||
"skip_calib_recovery": args.skip_calib_recovery,
|
||||
"only_redownload_affected_cases": args.only_redownload_affected_cases,
|
||||
"issue_filter": args.issue_ids or [],
|
||||
"summary": summary,
|
||||
"actions": [action.to_dict() for action in action_results],
|
||||
}
|
||||
|
||||
|
||||
def print_summary(manifest: dict) -> None:
|
||||
print(f"input_json: {manifest['input_json']}")
|
||||
print(f"output_root: {manifest['output_root']}")
|
||||
print(f"dry_run: {manifest['dry_run']}")
|
||||
for status, count in sorted(manifest["summary"].items()):
|
||||
print(f"{status}: {count}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
input_json = Path(args.input_json).resolve()
|
||||
output_root = Path(args.output_root).resolve()
|
||||
manifest_path = (
|
||||
Path(args.manifest_path).resolve()
|
||||
if args.manifest_path
|
||||
else output_root / "download_manifest.json"
|
||||
)
|
||||
|
||||
items = load_issue_items(input_json)
|
||||
if args.issue_ids:
|
||||
issue_filter = set(args.issue_ids)
|
||||
items = [item for item in items if int(item["id"]) in issue_filter]
|
||||
|
||||
ensure_dir(output_root, dry_run=args.dry_run)
|
||||
|
||||
action_results: list[ActionResult] = []
|
||||
total_items = len(items)
|
||||
log_progress(f"issues_to_process: {total_items}")
|
||||
for index, item in enumerate(items, start=1):
|
||||
issue_id = int(item["id"])
|
||||
issue_name = compact_text(item.get("name"), max_len=64)
|
||||
log_progress(f"[{index}/{total_items}] issue_{issue_id} start: {issue_name}")
|
||||
issue_results = process_issue(
|
||||
item=item,
|
||||
output_root=output_root,
|
||||
dry_run=args.dry_run,
|
||||
skip_mdi=args.skip_mdi,
|
||||
skip_copy=args.skip_copy,
|
||||
only_redownload_affected_cases=args.only_redownload_affected_cases,
|
||||
skip_calib_recovery=args.skip_calib_recovery,
|
||||
)
|
||||
action_results.extend(issue_results)
|
||||
log_progress(
|
||||
f"[{index}/{total_items}] issue_{issue_id} done: {summarize_issue_results(issue_results)}"
|
||||
)
|
||||
|
||||
manifest = build_manifest(args, input_json, output_root, action_results)
|
||||
ensure_dir(manifest_path.parent, dry_run=args.dry_run)
|
||||
if not args.dry_run:
|
||||
manifest_path.write_text(
|
||||
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
print_summary(manifest)
|
||||
if args.dry_run:
|
||||
print(f"manifest (not written in dry-run): {manifest_path}")
|
||||
else:
|
||||
print(f"manifest: {manifest_path}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
38
tools/feishu_project/download_issue_data.sh
Executable file
38
tools/feishu_project/download_issue_data.sh
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
PROJECT_ROOT=$(cd "${SCRIPT_DIR}/../.." && pwd)
|
||||
|
||||
SYNC_ROOT=${SYNC_ROOT:-/data1/dongying/Mono3d/G1Q3/feishu_project}
|
||||
DEFAULT_INPUT_JSON="${SYNC_ROOT}/exports/dongying_g1q3_issue_list.json"
|
||||
FALLBACK_INPUT_JSON="${PROJECT_ROOT}/tools/feishu_project/dongying_g1q3_issue_list.json"
|
||||
INPUT_JSON=${INPUT_JSON:-"${DEFAULT_INPUT_JSON}"}
|
||||
OUTPUT_ROOT=${OUTPUT_ROOT:-/data1/dongying/Mono3d/G1Q3/feishu_project/downloaded_issue_data}
|
||||
MANIFEST_PATH=${MANIFEST_PATH:-"${OUTPUT_ROOT}/download_manifest.json"}
|
||||
PYTHON_BIN=${PYTHON_BIN:-/deeplearning_team/ydong/dongying/miniconda/envs/dev/bin/python}
|
||||
|
||||
if [[ ! -f "${INPUT_JSON}" && -f "${FALLBACK_INPUT_JSON}" ]]; then
|
||||
INPUT_JSON="${FALLBACK_INPUT_JSON}"
|
||||
fi
|
||||
|
||||
EXTRA_ARGS=()
|
||||
if [[ "${DRY_RUN:-0}" == "1" ]]; then
|
||||
EXTRA_ARGS+=("--dry-run")
|
||||
fi
|
||||
if [[ "${SKIP_MDI:-0}" == "1" ]]; then
|
||||
EXTRA_ARGS+=("--skip-mdi")
|
||||
fi
|
||||
if [[ "${SKIP_COPY:-0}" == "1" ]]; then
|
||||
EXTRA_ARGS+=("--skip-copy")
|
||||
fi
|
||||
if [[ "${ONLY_REDOWNLOAD_AFFECTED_CASES:-0}" == "1" ]]; then
|
||||
EXTRA_ARGS+=("--only-redownload-affected-cases")
|
||||
fi
|
||||
|
||||
"${PYTHON_BIN}" "${PROJECT_ROOT}/tools/feishu_project/download_issue_data.py" \
|
||||
--input-json "${INPUT_JSON}" \
|
||||
--output-root "${OUTPUT_ROOT}" \
|
||||
--manifest-path "${MANIFEST_PATH}" \
|
||||
"${EXTRA_ARGS[@]}" \
|
||||
"$@"
|
||||
209
tools/feishu_project/export_feishu_view_issues.py
Executable file
209
tools/feishu_project/export_feishu_view_issues.py
Executable file
@@ -0,0 +1,209 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Export Feishu view issues and enrich them with selected detail fields."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
DEFAULT_EXTRA_FIELDS = [
|
||||
"缺陷标签池",
|
||||
"问题数据地址",
|
||||
"问题数据地址_PDCL",
|
||||
"问题发生frameid",
|
||||
]
|
||||
|
||||
|
||||
def run_fp(args: list[str]) -> dict:
|
||||
result = subprocess.run(
|
||||
["fp", *args],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
if result.returncode != 0:
|
||||
stderr = result.stderr.strip()
|
||||
stdout = result.stdout.strip()
|
||||
detail = stderr or stdout or "unknown error"
|
||||
raise RuntimeError(f"fp command failed: {' '.join(args)}\n{detail}")
|
||||
try:
|
||||
return json.loads(result.stdout)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise RuntimeError(
|
||||
f"fp returned non-JSON output for command: {' '.join(args)}"
|
||||
) from exc
|
||||
|
||||
|
||||
def fetch_view(project_key: str, user_key: str, view_name: str, work_item_type: str) -> dict | None:
|
||||
payload = run_fp(
|
||||
[
|
||||
"view",
|
||||
"list",
|
||||
"-o",
|
||||
"json",
|
||||
"-p",
|
||||
project_key,
|
||||
"-u",
|
||||
user_key,
|
||||
"-t",
|
||||
work_item_type,
|
||||
"--name",
|
||||
view_name,
|
||||
]
|
||||
)
|
||||
views = payload.get("data", [])
|
||||
if not views:
|
||||
return None
|
||||
return views[0]
|
||||
|
||||
|
||||
def fetch_items(project_key: str, user_key: str, view_name: str, work_item_type: str) -> dict:
|
||||
return run_fp(
|
||||
[
|
||||
"workitem",
|
||||
"list",
|
||||
"-o",
|
||||
"json",
|
||||
"--all",
|
||||
"-p",
|
||||
project_key,
|
||||
"-u",
|
||||
user_key,
|
||||
"-t",
|
||||
work_item_type,
|
||||
"--view",
|
||||
view_name,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def fetch_item_detail(project_key: str, user_key: str, item_id: int, work_item_type: str) -> dict:
|
||||
payload = run_fp(
|
||||
[
|
||||
"workitem",
|
||||
"get",
|
||||
str(item_id),
|
||||
"-o",
|
||||
"json",
|
||||
"-p",
|
||||
project_key,
|
||||
"-u",
|
||||
user_key,
|
||||
"-t",
|
||||
work_item_type,
|
||||
]
|
||||
)
|
||||
return payload["data"]
|
||||
|
||||
|
||||
def enrich_items(
|
||||
base_items: list[dict],
|
||||
project_key: str,
|
||||
user_key: str,
|
||||
work_item_type: str,
|
||||
extra_fields: list[str],
|
||||
) -> list[dict]:
|
||||
enriched = []
|
||||
for item in base_items:
|
||||
detail = fetch_item_detail(project_key, user_key, item["id"], work_item_type)
|
||||
detail_fields = detail.get("fields", {})
|
||||
merged = dict(item)
|
||||
for field_name in extra_fields:
|
||||
merged[field_name] = detail_fields.get(field_name)
|
||||
enriched.append(merged)
|
||||
return enriched
|
||||
|
||||
|
||||
def build_output(
|
||||
project_key: str,
|
||||
user_key: str,
|
||||
view_name: str,
|
||||
view: dict | None,
|
||||
items_payload: dict,
|
||||
extra_fields: list[str],
|
||||
work_item_type: str,
|
||||
) -> dict:
|
||||
return {
|
||||
"exported_at": datetime.now().astimezone().isoformat(timespec="seconds"),
|
||||
"source": {
|
||||
"project_key": project_key,
|
||||
"user_key": user_key,
|
||||
"view_name": view_name,
|
||||
"view_id": None if view is None else view.get("view_id"),
|
||||
"work_item_type": work_item_type,
|
||||
"included_detail_fields": extra_fields,
|
||||
},
|
||||
"summary": {
|
||||
"success": items_payload.get("success", False),
|
||||
"page": items_payload.get("page"),
|
||||
"total": items_payload.get("total"),
|
||||
},
|
||||
"items": items_payload["data"],
|
||||
}
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--project-key", required=True)
|
||||
parser.add_argument("--user-key", required=True)
|
||||
parser.add_argument("--view-name", required=True)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
required=True,
|
||||
type=Path,
|
||||
help="Path to the exported JSON file.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--extra-field",
|
||||
action="append",
|
||||
dest="extra_fields",
|
||||
help="Extra detail field to include. Can be repeated.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--work-item-type",
|
||||
default="issue",
|
||||
help="Feishu work item type. Defaults to issue.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
extra_fields = args.extra_fields or list(DEFAULT_EXTRA_FIELDS)
|
||||
|
||||
view = fetch_view(args.project_key, args.user_key, args.view_name, args.work_item_type)
|
||||
items_payload = fetch_items(args.project_key, args.user_key, args.view_name, args.work_item_type)
|
||||
items_payload["data"] = enrich_items(
|
||||
items_payload.get("data", []),
|
||||
args.project_key,
|
||||
args.user_key,
|
||||
args.work_item_type,
|
||||
extra_fields,
|
||||
)
|
||||
|
||||
output = build_output(
|
||||
args.project_key,
|
||||
args.user_key,
|
||||
args.view_name,
|
||||
view,
|
||||
items_payload,
|
||||
extra_fields,
|
||||
args.work_item_type,
|
||||
)
|
||||
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(
|
||||
json.dumps(output, ensure_ascii=False, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
66
tools/feishu_project/feishu_project.md
Executable file
66
tools/feishu_project/feishu_project.md
Executable file
@@ -0,0 +1,66 @@
|
||||
`tools/feishu_project` 用来维护飞书问题视图导出、问题数据下载、批量推理,以及问题 case 的后处理脚本。
|
||||
|
||||
当前默认项目配置:
|
||||
- `project_key`: `68ef617fb371dc80a10641f7`
|
||||
- `user_key`: `7550145433285312514`
|
||||
- `view_name`: `董颖-G1Q3`
|
||||
|
||||
推荐入口
|
||||
- 全链路增量同步:`tools/feishu_project/sync_g1q3_issue_data.sh`
|
||||
- CNCAP case 文本清单批处理:`tools/feishu_project/run_cncap_case_pipeline.sh`
|
||||
- 只导出视图:`tools/feishu_project/export_feishu_view_issues.py`
|
||||
- CNCAP case 清单转 synthetic issue JSON:`tools/feishu_project/build_cncap_case_issue_json.py`
|
||||
- 只下载数据:`tools/feishu_project/download_issue_data.sh`
|
||||
- 只跑推理:`tools/feishu_project/run_issue_data_inference.sh`
|
||||
- 问题标签画像统计:`tools/feishu_project/run_issue_tag_profile.sh`
|
||||
- 发布问题标签画像内网页面:`tools/feishu_project/publish_issue_tag_profile_site.sh`
|
||||
- 启动轻量静态服务:`tools/feishu_project/serve_issue_tag_profile_site.sh`
|
||||
- 跟踪与后处理:`tools/feishu_project/run_issue_data_tracking.sh`
|
||||
|
||||
目录约定
|
||||
- 此目录优先放源码和轻量文档,不再推荐把运行产物直接落在这里。
|
||||
- 默认运行产物应写到 `/data1/dongying/Mono3d/*/feishu_project/` 下的 `exports/`、`downloaded_issue_data/`、`inference_issue_data/`、`reports/` 等目录。
|
||||
- `__pycache__/`、HTML 报告、一次性导出 JSON/Markdown 结果属于运行产物或分析产物,除非明确要做样例留存,否则不要继续堆在 `tools/feishu_project/` 根目录。
|
||||
|
||||
维护原则
|
||||
- 通用脚本不要保留单个 case、单个 issue 的硬编码默认值。
|
||||
- G1Q3 包装脚本优先读取同步目录下的最新导出结果,不依赖仓库内静态 JSON。
|
||||
- 复杂逻辑尽量收敛在 Python 主脚本里,Shell 脚本只保留参数拼装和流程编排。
|
||||
|
||||
常用命令
|
||||
```bash
|
||||
# 查看 fp 帮助
|
||||
fp --help
|
||||
|
||||
# 更新 fp
|
||||
fp selfupdate
|
||||
|
||||
# 增量同步 G1Q3 问题并执行下载/推理/跟踪
|
||||
bash tools/feishu_project/sync_g1q3_issue_data.sh
|
||||
|
||||
# 只下载指定 issue
|
||||
bash tools/feishu_project/download_issue_data.sh --issue-id 6965833173
|
||||
|
||||
# 只对指定 issue 跑推理
|
||||
bash tools/feishu_project/run_issue_data_inference.sh --issue-id 6965833173
|
||||
|
||||
# 基于最新 G1Q3 导出生成目标/问题标签画像
|
||||
bash tools/feishu_project/run_issue_tag_profile.sh
|
||||
|
||||
# 生成标签画像并发布到静态站点目录
|
||||
bash tools/feishu_project/publish_issue_tag_profile_site.sh
|
||||
|
||||
# 无 Nginx 时临时启动内网静态服务
|
||||
bash tools/feishu_project/serve_issue_tag_profile_site.sh start
|
||||
|
||||
# 指定内网访问地址。该 IP 必须是当前机器网卡 IP,或由反向代理/内网服务器承载。
|
||||
INTRANET_HOST=192.168.2.169 bash tools/feishu_project/publish_issue_tag_profile_site.sh
|
||||
INTRANET_HOST=192.168.2.169 bash tools/feishu_project/serve_issue_tag_profile_site.sh restart
|
||||
```
|
||||
|
||||
内网页面发布
|
||||
- 默认发布路径:`/data1/dongying/Mono3d/G1Q3/feishu_project/site/issue_tag_profile/index.html`
|
||||
- 默认访问路径:`http://<内网机器IP>:8088/issue_tag_profile/`
|
||||
- Nginx 示例配置:`tools/feishu_project/nginx_issue_tag_profile.conf.example`
|
||||
- 临时静态服务脚本:`tools/feishu_project/serve_issue_tag_profile_site.sh`
|
||||
- 可用 `INTRANET_HOST=<内网IP>` 或 `PUBLIC_HOST=<内网IP>` 指定页面里展示的访问地址;如果该 IP 不在当前机器网卡上,脚本会提示需要部署到对应内网机器或配置反向代理。
|
||||
178
tools/feishu_project/recover_downloaded_issue_calibs.py
Executable file
178
tools/feishu_project/recover_downloaded_issue_calibs.py
Executable file
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Recover missing camera4.json files for downloaded standard-path issue data."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
FILE = Path(__file__).resolve()
|
||||
ROOT = FILE.parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.append(str(ROOT))
|
||||
|
||||
from tools.feishu_project.case_calib_recovery import CalibRecoveryResult, recover_camera4_json
|
||||
|
||||
|
||||
DEFAULT_DOWNLOAD_ROOT = Path("/data1/dongying/Mono3d/G1Q3/feishu_project/downloaded_issue_data")
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--download-root",
|
||||
default=str(DEFAULT_DOWNLOAD_ROOT),
|
||||
help="Root directory containing downloaded issue data.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--issue-id",
|
||||
action="append",
|
||||
dest="issue_ids",
|
||||
type=int,
|
||||
help="Only process the specified issue id. Can be repeated.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--path-root",
|
||||
action="append",
|
||||
default=[],
|
||||
help=(
|
||||
"Optional explicit copied target root to recover. Can be repeated. "
|
||||
"Example: downloaded_issue_data/issue_x/path_01/case_dir"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--manifest-path",
|
||||
default="",
|
||||
help="Optional explicit manifest path. Defaults to <download-root>/calib_recovery_manifest.json",
|
||||
)
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def log_progress(message: str) -> None:
|
||||
timestamp = datetime.now().astimezone().strftime("%Y-%m-%d %H:%M:%S")
|
||||
print(f"[recover_downloaded_issue_calibs {timestamp}] {message}", flush=True)
|
||||
|
||||
|
||||
def discover_target_roots(download_root: Path, issue_ids: list[int], explicit_roots: list[str]) -> list[Path]:
|
||||
if explicit_roots:
|
||||
return sorted({Path(path).resolve() for path in explicit_roots})
|
||||
|
||||
issue_dirs: list[Path]
|
||||
if issue_ids:
|
||||
issue_dirs = [download_root / f"issue_{issue_id}" for issue_id in sorted(set(issue_ids))]
|
||||
else:
|
||||
issue_dirs = sorted(path for path in download_root.glob("issue_*") if path.is_dir())
|
||||
|
||||
targets: list[Path] = []
|
||||
seen: set[Path] = set()
|
||||
for issue_dir in issue_dirs:
|
||||
if not issue_dir.is_dir():
|
||||
continue
|
||||
for path_dir in sorted(path for path in issue_dir.glob("path_*") if path.is_dir()):
|
||||
direct_case = path_dir / "sigmastar.1" / "camera4.bin"
|
||||
if direct_case.is_file() and path_dir not in seen:
|
||||
seen.add(path_dir)
|
||||
targets.append(path_dir)
|
||||
|
||||
for child in sorted(path_dir.iterdir()):
|
||||
if not child.is_dir() or child.name == "test_data":
|
||||
continue
|
||||
if child in seen:
|
||||
continue
|
||||
seen.add(child)
|
||||
targets.append(child)
|
||||
return targets
|
||||
|
||||
|
||||
def result_to_dict(target_root: Path, result: CalibRecoveryResult) -> dict:
|
||||
return {
|
||||
"target_root": str(target_root),
|
||||
"target_path": str(result.target_path),
|
||||
"status": result.status,
|
||||
"detail": result.detail,
|
||||
"source_path": None if result.source_path is None else str(result.source_path),
|
||||
"source_kind": result.source_kind,
|
||||
}
|
||||
|
||||
|
||||
def build_manifest(
|
||||
download_root: Path,
|
||||
dry_run: bool,
|
||||
issue_ids: list[int],
|
||||
target_roots: list[Path],
|
||||
results: list[dict],
|
||||
) -> dict:
|
||||
summary: dict[str, int] = {}
|
||||
for result in results:
|
||||
status = result["status"]
|
||||
summary[status] = summary.get(status, 0) + 1
|
||||
|
||||
return {
|
||||
"generated_at": datetime.now().astimezone().isoformat(timespec="seconds"),
|
||||
"download_root": str(download_root),
|
||||
"dry_run": dry_run,
|
||||
"issue_filter": issue_ids,
|
||||
"target_roots": [str(path) for path in target_roots],
|
||||
"summary": summary,
|
||||
"results": results,
|
||||
}
|
||||
|
||||
|
||||
def print_summary(manifest: dict) -> None:
|
||||
print(f"download_root: {manifest['download_root']}")
|
||||
print(f"dry_run: {manifest['dry_run']}")
|
||||
print(f"target_roots: {len(manifest['target_roots'])}")
|
||||
for status, count in sorted(manifest["summary"].items()):
|
||||
print(f"{status}: {count}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
download_root = Path(args.download_root).resolve()
|
||||
manifest_path = (
|
||||
Path(args.manifest_path).resolve()
|
||||
if args.manifest_path
|
||||
else download_root / "calib_recovery_manifest.json"
|
||||
)
|
||||
target_roots = discover_target_roots(download_root, args.issue_ids or [], args.path_root)
|
||||
log_progress(f"targets_to_process: {len(target_roots)}")
|
||||
|
||||
results: list[dict] = []
|
||||
for index, target_root in enumerate(target_roots, start=1):
|
||||
log_progress(f"[{index}/{len(target_roots)}] start: {target_root}")
|
||||
recovery = recover_camera4_json(
|
||||
source_root=target_root,
|
||||
target_root=target_root,
|
||||
dry_run=args.dry_run,
|
||||
)
|
||||
results.append(result_to_dict(target_root, recovery))
|
||||
log_progress(f"[{index}/{len(target_roots)}] done: {recovery.status}")
|
||||
|
||||
manifest = build_manifest(
|
||||
download_root=download_root,
|
||||
dry_run=args.dry_run,
|
||||
issue_ids=args.issue_ids or [],
|
||||
target_roots=target_roots,
|
||||
results=results,
|
||||
)
|
||||
if not args.dry_run:
|
||||
manifest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
manifest_path.write_text(
|
||||
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
print_summary(manifest)
|
||||
if args.dry_run:
|
||||
print(f"manifest (not written in dry-run): {manifest_path}")
|
||||
else:
|
||||
print(f"manifest: {manifest_path}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
330
tools/feishu_project/run_cncap_case_pipeline.sh
Executable file
330
tools/feishu_project/run_cncap_case_pipeline.sh
Executable file
@@ -0,0 +1,330 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
PROJECT_ROOT=$(cd "${SCRIPT_DIR}/../.." && pwd)
|
||||
|
||||
CASE_LIST=${CASE_LIST:-"${PROJECT_ROOT}/tools/feishu_project/cncap_case.txt"}
|
||||
SYNC_ROOT=${SYNC_ROOT:-/data1/dongying/Mono3d/CNCAP/feishu_project}
|
||||
EXPORT_JSON=${EXPORT_JSON:-"${SYNC_ROOT}/exports/cncap_case_issue_list.json"}
|
||||
CASE_INDEX_JSON=${CASE_INDEX_JSON:-"${SYNC_ROOT}/exports/cncap_case_issue_index.json"}
|
||||
DOWNLOAD_ROOT=${DOWNLOAD_ROOT:-"${SYNC_ROOT}/downloaded_issue_data"}
|
||||
DOWNLOAD_MANIFEST_PATH=${DOWNLOAD_MANIFEST_PATH:-"${DOWNLOAD_ROOT}/download_manifest.json"}
|
||||
INFERENCE_ROOT=${INFERENCE_ROOT:-"${SYNC_ROOT}/inference_issue_data"}
|
||||
INFERENCE_MANIFEST_PATH=${INFERENCE_MANIFEST_PATH:-"${INFERENCE_ROOT}/inference_manifest.json"}
|
||||
|
||||
PYTHON_BIN=${PYTHON_BIN:-/deeplearning_team/ydong/dongying/miniconda/envs/dev/bin/python}
|
||||
RUN_BUILD=${RUN_BUILD:-1}
|
||||
RUN_DOWNLOAD=${RUN_DOWNLOAD:-1}
|
||||
RUN_INFERENCE=${RUN_INFERENCE:-1}
|
||||
ENABLE_TRACKING=${ENABLE_TRACKING:-1}
|
||||
ENABLE_CONVERT=${ENABLE_CONVERT:-1}
|
||||
ENABLE_VISUALIZE=${ENABLE_VISUALIZE:-0}
|
||||
ENABLE_TEMPORAL_ANALYSIS=${ENABLE_TEMPORAL_ANALYSIS:-0}
|
||||
SKIP_EXISTING_INFERENCE=${SKIP_EXISTING_INFERENCE:-1}
|
||||
DRY_RUN=${DRY_RUN:-0}
|
||||
|
||||
USE_ISSUE_FRAME_WINDOW=${USE_ISSUE_FRAME_WINDOW:-0}
|
||||
FRAME_BEFORE=${FRAME_BEFORE:-200}
|
||||
FRAME_AFTER=${FRAME_AFTER:-200}
|
||||
MISSING_ISSUE_FRAME_POLICY=${MISSING_ISSUE_FRAME_POLICY:-full}
|
||||
FRAME_INDEX_START=${FRAME_INDEX_START:-}
|
||||
FRAME_INDEX_END=${FRAME_INDEX_END:-}
|
||||
FRAME_ID_START=${FRAME_ID_START:-}
|
||||
FRAME_ID_END=${FRAME_ID_END:-}
|
||||
TARGET_FRAME_ID=${TARGET_FRAME_ID:-}
|
||||
|
||||
VIDEO_STRIDE=${VIDEO_STRIDE:-1}
|
||||
MAX_IMAGES=${MAX_IMAGES:-0}
|
||||
EXPORTED_MODEL=${EXPORTED_MODEL:-${PROJECT_ROOT}/runs/export/train_mono3d_two_roi_20260416-raw_no_edge/merged_model.torchscript}
|
||||
DEVICE=${DEVICE:-}
|
||||
PROVIDERS=${PROVIDERS:-}
|
||||
ENABLE_ATTR=${ENABLE_ATTR:-0}
|
||||
ENABLE_CROSS_CLASS_MERGE_PRIOR=${ENABLE_CROSS_CLASS_MERGE_PRIOR:-1}
|
||||
SAVE_AGGREGATE_PREDICTIONS=${SAVE_AGGREGATE_PREDICTIONS:-1}
|
||||
INFERENCE_EXTRA_ARGS=${INFERENCE_EXTRA_ARGS:-}
|
||||
|
||||
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:-}
|
||||
MERGE_OUTPUT_NAME=${MERGE_OUTPUT_NAME:-combined_tracking.json}
|
||||
FILE_PATTERN=${FILE_PATTERN:-*.json}
|
||||
ENABLE_USE_3D=${ENABLE_USE_3D:-0}
|
||||
CONVERT_OUTPUT_DIR_NAME=${CONVERT_OUTPUT_DIR_NAME:-objectlist}
|
||||
CONVERT_TRACKING_JSON_NAME=${CONVERT_TRACKING_JSON_NAME:-merge.json}
|
||||
CONVERT_CAM_ID=${CONVERT_CAM_ID:-}
|
||||
VIS_DOWNLOAD_ROOT=${VIS_DOWNLOAD_ROOT:-"${DOWNLOAD_ROOT}"}
|
||||
VIS_OUTPUT_ROOT=${VIS_OUTPUT_ROOT:-}
|
||||
VIS_OUTPUT_DIR_NAME=${VIS_OUTPUT_DIR_NAME:-tracking_vis_raw}
|
||||
VIS_TRACKING_JSON_NAME=${VIS_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}
|
||||
TEMPORAL_OUTPUT_ROOT=${TEMPORAL_OUTPUT_ROOT:-}
|
||||
TEMPORAL_OUTPUT_DIR_NAME=${TEMPORAL_OUTPUT_DIR_NAME:-temporal_observation}
|
||||
TEMPORAL_JSON_NAME=${TEMPORAL_JSON_NAME:-merge.json}
|
||||
TEMPORAL_MIN_LENGTH=${TEMPORAL_MIN_LENGTH:-3}
|
||||
TEMPORAL_TRACK_IDS=${TEMPORAL_TRACK_IDS:-}
|
||||
TEMPORAL_CLASS_ID=${TEMPORAL_CLASS_ID:-}
|
||||
TEMPORAL_FRAME_ID_START=${TEMPORAL_FRAME_ID_START:-}
|
||||
TEMPORAL_FRAME_ID_END=${TEMPORAL_FRAME_ID_END:-}
|
||||
TEMPORAL_PLOTS=${TEMPORAL_PLOTS:-0}
|
||||
TEMPORAL_EXPORT_SERIES=${TEMPORAL_EXPORT_SERIES:-1}
|
||||
TEMPORAL_FOCUS_TRACK_PLOTS=${TEMPORAL_FOCUS_TRACK_PLOTS:-1}
|
||||
TEMPORAL_PREFER_EGO=${TEMPORAL_PREFER_EGO:-1}
|
||||
TEMPORAL_X_AXIS=${TEMPORAL_X_AXIS:-frame_id}
|
||||
TEMPORAL_HEADING_SOURCE=${TEMPORAL_HEADING_SOURCE:-camera_reg}
|
||||
TRACKING_MODEL_VERSION=${TRACKING_MODEL_VERSION:-}
|
||||
|
||||
SKIP_MDI=${SKIP_MDI:-0}
|
||||
SKIP_COPY=${SKIP_COPY:-0}
|
||||
ONLY_REDOWNLOAD_AFFECTED_CASES=${ONLY_REDOWNLOAD_AFFECTED_CASES:-0}
|
||||
ID_BASE=${ID_BASE:-9000000000}
|
||||
|
||||
CASE_JSON_BUILDER=${CASE_JSON_BUILDER:-"${PROJECT_ROOT}/tools/feishu_project/build_cncap_case_issue_json.py"}
|
||||
DOWNLOAD_WRAPPER=${DOWNLOAD_WRAPPER:-"${PROJECT_ROOT}/tools/feishu_project/download_issue_data.sh"}
|
||||
INFERENCE_WRAPPER=${INFERENCE_WRAPPER:-"${PROJECT_ROOT}/tools/feishu_project/run_issue_data_inference.sh"}
|
||||
TRACKING_WRAPPER=${TRACKING_WRAPPER:-"${PROJECT_ROOT}/tools/feishu_project/run_issue_data_tracking.sh"}
|
||||
|
||||
ISSUE_ID_ARGS=()
|
||||
ISSUE_ID_VALUES=()
|
||||
|
||||
show_usage() {
|
||||
cat <<EOF
|
||||
Usage:
|
||||
bash $(basename "${BASH_SOURCE[0]}") [--issue-id <id>]...
|
||||
|
||||
Environment variables:
|
||||
CASE_LIST, SYNC_ROOT, EXPORT_JSON, CASE_INDEX_JSON
|
||||
DOWNLOAD_ROOT, INFERENCE_ROOT, EXPORTED_MODEL
|
||||
RUN_BUILD, RUN_DOWNLOAD, RUN_INFERENCE, ENABLE_TRACKING
|
||||
ENABLE_CONVERT, ENABLE_VISUALIZE, ENABLE_TEMPORAL_ANALYSIS
|
||||
DRY_RUN, ID_BASE
|
||||
EOF
|
||||
}
|
||||
|
||||
while (($# > 0)); do
|
||||
case "$1" in
|
||||
--issue-id)
|
||||
if (($# < 2)); then
|
||||
echo "Error: --issue-id requires a value" >&2
|
||||
exit 1
|
||||
fi
|
||||
ISSUE_ID_ARGS+=("$1" "$2")
|
||||
ISSUE_ID_VALUES+=("$2")
|
||||
shift 2
|
||||
;;
|
||||
--issue-id=*)
|
||||
issue_id="${1#*=}"
|
||||
ISSUE_ID_ARGS+=("$1")
|
||||
ISSUE_ID_VALUES+=("${issue_id}")
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
show_usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Error: unsupported option: $1" >&2
|
||||
show_usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
print_stage() {
|
||||
local stage_name="$1"
|
||||
echo ""
|
||||
echo "######################################################################"
|
||||
echo "# ${stage_name}"
|
||||
echo "######################################################################"
|
||||
}
|
||||
|
||||
resolve_tracking_model_version() {
|
||||
if [[ -n "${TRACKING_MODEL_VERSION}" ]]; then
|
||||
printf '%s\n' "${TRACKING_MODEL_VERSION}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -n "${EXPORTED_MODEL}" ]] && [[ "${EXPORTED_MODEL}" =~ ([0-9]{8}) ]]; then
|
||||
printf '%s\n' "${BASH_REMATCH[1]}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
has_local_video_cases() {
|
||||
local search_root="$1"
|
||||
shift || true
|
||||
|
||||
if [[ ! -d "${search_root}" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ "$#" -eq 0 ]]; then
|
||||
[[ -n "$(find "${search_root}" -type f -path '*/sigmastar.1/camera4.bin' -print -quit)" ]]
|
||||
return
|
||||
fi
|
||||
|
||||
local issue_id=""
|
||||
for issue_id in "$@"; do
|
||||
if [[ -d "${search_root}/issue_${issue_id}" ]] && [[ -n "$(find "${search_root}/issue_${issue_id}" -type f -path '*/sigmastar.1/camera4.bin' -print -quit)" ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
TMP_ROOT=""
|
||||
EFFECTIVE_EXPORT_JSON="${EXPORT_JSON}"
|
||||
EFFECTIVE_CASE_INDEX_JSON="${CASE_INDEX_JSON}"
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "${TMP_ROOT}" && -d "${TMP_ROOT}" ]]; then
|
||||
rm -rf "${TMP_ROOT}"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
if [[ "${DRY_RUN}" == "1" && "${RUN_BUILD}" == "1" ]]; then
|
||||
TMP_ROOT=$(mktemp -d /tmp/cncap_case_pipeline.XXXXXX)
|
||||
EFFECTIVE_EXPORT_JSON="${TMP_ROOT}/cncap_case_issue_list.json"
|
||||
EFFECTIVE_CASE_INDEX_JSON="${TMP_ROOT}/cncap_case_issue_index.json"
|
||||
fi
|
||||
|
||||
export PYTHON_BIN
|
||||
export TRACK_CLASSES
|
||||
export IOU_THRESH
|
||||
export MAX_AGE
|
||||
export MIN_HITS
|
||||
export DIST_THRESH
|
||||
export MAX_3D_DISTANCE
|
||||
export MAX_FRAMES
|
||||
export MERGE_OUTPUT_NAME
|
||||
export FILE_PATTERN
|
||||
export ENABLE_USE_3D
|
||||
export ENABLE_CONVERT
|
||||
export ENABLE_VISUALIZE
|
||||
export ENABLE_TEMPORAL_ANALYSIS
|
||||
export CONVERT_OUTPUT_DIR_NAME
|
||||
export CONVERT_TRACKING_JSON_NAME
|
||||
export CONVERT_CAM_ID
|
||||
export VIS_DOWNLOAD_ROOT
|
||||
export VIS_OUTPUT_ROOT
|
||||
export VIS_OUTPUT_DIR_NAME
|
||||
export VIS_TRACKING_JSON_NAME
|
||||
export VIS_MAX_FRAMES
|
||||
export VIS_CLASS_ID
|
||||
export VIS_TRACK_IDS
|
||||
export VIS_SHOW_TRAJECTORY
|
||||
export TEMPORAL_OUTPUT_ROOT
|
||||
export TEMPORAL_OUTPUT_DIR_NAME
|
||||
export TEMPORAL_JSON_NAME
|
||||
export TEMPORAL_MIN_LENGTH
|
||||
export TEMPORAL_TRACK_IDS
|
||||
export TEMPORAL_CLASS_ID
|
||||
export TEMPORAL_FRAME_ID_START
|
||||
export TEMPORAL_FRAME_ID_END
|
||||
export TEMPORAL_PLOTS
|
||||
export TEMPORAL_EXPORT_SERIES
|
||||
export TEMPORAL_FOCUS_TRACK_PLOTS
|
||||
export TEMPORAL_PREFER_EGO
|
||||
export TEMPORAL_X_AXIS
|
||||
export TEMPORAL_HEADING_SOURCE
|
||||
|
||||
if [[ "${RUN_BUILD}" == "1" ]]; then
|
||||
print_stage "Build CNCAP Synthetic Issue JSON"
|
||||
echo "CASE_LIST : ${CASE_LIST}"
|
||||
echo "EXPORT_JSON : ${EFFECTIVE_EXPORT_JSON}"
|
||||
echo "CASE_INDEX_JSON : ${EFFECTIVE_CASE_INDEX_JSON}"
|
||||
echo "ID_BASE : ${ID_BASE}"
|
||||
|
||||
"${PYTHON_BIN}" "${CASE_JSON_BUILDER}" \
|
||||
--input "${CASE_LIST}" \
|
||||
--output "${EFFECTIVE_EXPORT_JSON}" \
|
||||
--case-index-output "${EFFECTIVE_CASE_INDEX_JSON}" \
|
||||
--id-base "${ID_BASE}"
|
||||
elif [[ ! -f "${EFFECTIVE_EXPORT_JSON}" ]]; then
|
||||
echo "Error: export json does not exist and RUN_BUILD=0: ${EFFECTIVE_EXPORT_JSON}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${RUN_DOWNLOAD}" == "1" ]]; then
|
||||
print_stage "Download CNCAP Case Data"
|
||||
INPUT_JSON="${EFFECTIVE_EXPORT_JSON}" \
|
||||
OUTPUT_ROOT="${DOWNLOAD_ROOT}" \
|
||||
MANIFEST_PATH="${DOWNLOAD_MANIFEST_PATH}" \
|
||||
PYTHON_BIN="${PYTHON_BIN}" \
|
||||
DRY_RUN="${DRY_RUN}" \
|
||||
SKIP_MDI="${SKIP_MDI}" \
|
||||
SKIP_COPY="${SKIP_COPY}" \
|
||||
ONLY_REDOWNLOAD_AFFECTED_CASES="${ONLY_REDOWNLOAD_AFFECTED_CASES}" \
|
||||
bash "${DOWNLOAD_WRAPPER}" "${ISSUE_ID_ARGS[@]}"
|
||||
fi
|
||||
|
||||
if [[ "${RUN_INFERENCE}" == "1" ]]; then
|
||||
if [[ "${DRY_RUN}" == "1" ]] && ! has_local_video_cases "${DOWNLOAD_ROOT}" "${ISSUE_ID_VALUES[@]}"; then
|
||||
print_stage "Skip CNCAP Inference"
|
||||
echo "Skipping inference in dry-run because no local downloaded camera4.bin cases were found."
|
||||
echo "Run download without DRY_RUN first, or reuse an existing DOWNLOAD_ROOT to plan inference."
|
||||
else
|
||||
print_stage "Run CNCAP Inference"
|
||||
DOWNLOAD_ROOT="${DOWNLOAD_ROOT}" \
|
||||
OUTPUT_ROOT="${INFERENCE_ROOT}" \
|
||||
MANIFEST_PATH="${INFERENCE_MANIFEST_PATH}" \
|
||||
PYTHON_BIN="${PYTHON_BIN}" \
|
||||
ISSUE_JSON="${EFFECTIVE_EXPORT_JSON}" \
|
||||
VIDEO_STRIDE="${VIDEO_STRIDE}" \
|
||||
MAX_IMAGES="${MAX_IMAGES}" \
|
||||
FRAME_INDEX_START="${FRAME_INDEX_START}" \
|
||||
FRAME_INDEX_END="${FRAME_INDEX_END}" \
|
||||
FRAME_ID_START="${FRAME_ID_START}" \
|
||||
FRAME_ID_END="${FRAME_ID_END}" \
|
||||
TARGET_FRAME_ID="${TARGET_FRAME_ID}" \
|
||||
FRAME_BEFORE="${FRAME_BEFORE}" \
|
||||
FRAME_AFTER="${FRAME_AFTER}" \
|
||||
USE_ISSUE_FRAME_WINDOW="${USE_ISSUE_FRAME_WINDOW}" \
|
||||
MISSING_ISSUE_FRAME_POLICY="${MISSING_ISSUE_FRAME_POLICY}" \
|
||||
EXPORTED_MODEL="${EXPORTED_MODEL}" \
|
||||
DEVICE="${DEVICE}" \
|
||||
PROVIDERS="${PROVIDERS}" \
|
||||
ENABLE_ATTR="${ENABLE_ATTR}" \
|
||||
ENABLE_CROSS_CLASS_MERGE_PRIOR="${ENABLE_CROSS_CLASS_MERGE_PRIOR}" \
|
||||
SAVE_AGGREGATE_PREDICTIONS="${SAVE_AGGREGATE_PREDICTIONS}" \
|
||||
SKIP_EXISTING="${SKIP_EXISTING_INFERENCE}" \
|
||||
DRY_RUN="${DRY_RUN}" \
|
||||
ENABLE_TRACKING=0 \
|
||||
INFERENCE_EXTRA_ARGS="${INFERENCE_EXTRA_ARGS}" \
|
||||
bash "${INFERENCE_WRAPPER}" "${ISSUE_ID_ARGS[@]}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "${ENABLE_TRACKING}" != "1" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${DRY_RUN}" == "1" ]]; then
|
||||
print_stage "Skip CNCAP Tracking"
|
||||
echo "Skipping tracking because DRY_RUN=1."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
TRACKING_MODEL_VERSION_RESOLVED=$(resolve_tracking_model_version || true)
|
||||
|
||||
print_stage "Run CNCAP Tracking/Convert Workflow"
|
||||
echo "INFERENCE_ROOT : ${INFERENCE_ROOT}"
|
||||
echo "VIS_DOWNLOAD_ROOT : ${VIS_DOWNLOAD_ROOT}"
|
||||
if [[ -n "${TRACKING_MODEL_VERSION_RESOLVED}" ]]; then
|
||||
echo "TRACKING_MODEL_VERSION : ${TRACKING_MODEL_VERSION_RESOLVED}"
|
||||
fi
|
||||
|
||||
RESULTS_ROOT="${INFERENCE_ROOT}" \
|
||||
PYTHON_BIN="${PYTHON_BIN}" \
|
||||
TRACKING_MODEL_VERSION="${TRACKING_MODEL_VERSION_RESOLVED}" \
|
||||
bash "${TRACKING_WRAPPER}" "${INFERENCE_ROOT}" "${ISSUE_ID_ARGS[@]}"
|
||||
186
tools/feishu_project/run_issue_data_convert_tracking.sh
Executable file
186
tools/feishu_project/run_issue_data_convert_tracking.sh
Executable file
@@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
PROJECT_ROOT=$(cd "${SCRIPT_DIR}/../.." && pwd)
|
||||
|
||||
RESULTS_ROOT=${RESULTS_ROOT:-/data1/dongying/Mono3d/G1Q3/feishu_project/inference_issue_data}
|
||||
OUTPUT_DIR_NAME=${OUTPUT_DIR_NAME:-objectlist}
|
||||
PYTHON_BIN=${PYTHON_BIN:-/deeplearning_team/ydong/dongying/miniconda/envs/dev/bin/python}
|
||||
CONVERT_LAUNCHER=${CONVERT_LAUNCHER:-"${PROJECT_ROOT}/tools/convert_merge_tracking_bundle/convert_merge_tracking_exported_onnx_infer_case.sh"}
|
||||
MERGE_JSON_NAME=${MERGE_JSON_NAME:-merge.json}
|
||||
CAM_ID=${CAM_ID:-}
|
||||
|
||||
TARGET_PATH=""
|
||||
ISSUE_IDS=()
|
||||
|
||||
while (($# > 0)); do
|
||||
case "$1" in
|
||||
--issue-id)
|
||||
if (($# < 2)); then
|
||||
echo "Error: --issue-id requires a value" >&2
|
||||
exit 1
|
||||
fi
|
||||
ISSUE_IDS+=("$2")
|
||||
shift 2
|
||||
;;
|
||||
--issue-id=*)
|
||||
ISSUE_IDS+=("${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
|
||||
|
||||
is_case_dir() {
|
||||
local dir_path="$1"
|
||||
[[ -d "${dir_path}" ]] && [[ -f "${dir_path}/${MERGE_JSON_NAME}" ]]
|
||||
}
|
||||
|
||||
is_predictions_dir() {
|
||||
local dir_path="$1"
|
||||
[[ -d "${dir_path}" ]] || return 1
|
||||
is_case_dir "$(dirname "${dir_path}")"
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
if [[ -f "${target_path}" ]] && [[ "$(basename "${target_path}")" == "${MERGE_JSON_NAME}" ]]; then
|
||||
dirname "${target_path}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
case_output_dir() {
|
||||
local case_dir="$1"
|
||||
printf '%s/%s\n' "${case_dir%/}" "${OUTPUT_DIR_NAME}"
|
||||
}
|
||||
|
||||
run_convert_case() {
|
||||
local case_dir="$1"
|
||||
local output_path
|
||||
|
||||
output_path="$(case_output_dir "${case_dir}")"
|
||||
|
||||
echo ""
|
||||
echo "######################################################################"
|
||||
echo "# Feishu issue-data protocol conversion"
|
||||
echo "######################################################################"
|
||||
echo "Case dir : ${case_dir}"
|
||||
echo "Tracking JSON: ${case_dir%/}/${MERGE_JSON_NAME}"
|
||||
echo "Output path : ${output_path}"
|
||||
if [[ -n "${CAM_ID}" ]]; then
|
||||
echo "Camera ID override: ${CAM_ID}"
|
||||
fi
|
||||
|
||||
PYTHON_BIN="${PYTHON_BIN}" \
|
||||
MERGE_JSON_NAME="${MERGE_JSON_NAME}" \
|
||||
CAM_ID="${CAM_ID}" \
|
||||
bash "${CONVERT_LAUNCHER}" "${case_dir}" "${output_path}"
|
||||
}
|
||||
|
||||
run_convert_root() {
|
||||
local target_root="$1"
|
||||
local total_cases=0
|
||||
local success_cases=0
|
||||
local failed_cases=0
|
||||
|
||||
echo ""
|
||||
echo "Batch-root mode: ${target_root}"
|
||||
|
||||
while IFS= read -r -d '' merge_json_path; do
|
||||
local case_dir
|
||||
case_dir="$(dirname "${merge_json_path}")"
|
||||
((total_cases += 1))
|
||||
|
||||
if run_convert_case "${case_dir}"; then
|
||||
((success_cases += 1))
|
||||
else
|
||||
((failed_cases += 1))
|
||||
printf '[FAIL] case=%s\n' "${case_dir}" >&2
|
||||
fi
|
||||
done < <(
|
||||
find "${target_root}" -type f -name "${MERGE_JSON_NAME}" -print0 | sort -z
|
||||
)
|
||||
|
||||
if [[ "${total_cases}" -eq 0 ]]; then
|
||||
echo "[ERROR] No ${MERGE_JSON_NAME} files were found under: ${target_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 [[ "${#ISSUE_IDS[@]}" -eq 0 ]]; then
|
||||
if RESOLVED_CASE_DIR="$(resolve_case_dir "${TARGET_PATH}")"; then
|
||||
run_convert_case "${RESOLVED_CASE_DIR}"
|
||||
else
|
||||
run_convert_root "${TARGET_PATH}"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
total_issues=0
|
||||
success_issues=0
|
||||
failed_issues=0
|
||||
|
||||
for issue_id in "${ISSUE_IDS[@]}"; do
|
||||
issue_root="${TARGET_PATH}/issue_${issue_id}"
|
||||
((total_issues += 1))
|
||||
|
||||
if [[ ! -d "${issue_root}" ]]; then
|
||||
echo "[FAIL] issue_${issue_id}: not found under ${TARGET_PATH}" >&2
|
||||
((failed_issues += 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
if run_convert_root "${issue_root}"; then
|
||||
((success_issues += 1))
|
||||
else
|
||||
((failed_issues += 1))
|
||||
echo "[FAIL] issue_${issue_id}: protocol conversion failed" >&2
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
printf '[DONE] issues=%d success=%d failed=%d\n' \
|
||||
"${total_issues}" "${success_issues}" "${failed_issues}"
|
||||
|
||||
[[ "${failed_issues}" -eq 0 ]]
|
||||
721
tools/feishu_project/run_issue_data_inference.py
Executable file
721
tools/feishu_project/run_issue_data_inference.py
Executable file
@@ -0,0 +1,721 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Batch inference runner for downloaded Feishu issue data."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
DEFAULT_DOWNLOAD_ROOT = Path("/data1/dongying/Mono3d/G1Q3/feishu_project/downloaded_issue_data")
|
||||
DEFAULT_OUTPUT_ROOT = Path("/data1/dongying/Mono3d/G1Q3/feishu_project/inference_issue_data")
|
||||
DEFAULT_INFERENCE_SCRIPT = ROOT / "tools" / "model_inference" / "core" / "run_two_roi_exported_onnx_infer.py"
|
||||
DEFAULT_PYTHON_BIN = Path("/deeplearning_team/ydong/dongying/miniconda/envs/dev/bin/python")
|
||||
ISSUE_DIR_RE = re.compile(r"^issue_(\d+)$")
|
||||
SIGNED_INTEGER_RE = re.compile(r"^[+-]?\d+$")
|
||||
FRAME_ID_CAMERA4_RE = re.compile(r"camera4\s*:\s*([+-]?\d+)", re.IGNORECASE)
|
||||
FRAME_ID_ANY_CAMERA_RE = re.compile(r"(camera\d+)\s*:\s*([+-]?\d+)", re.IGNORECASE)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TargetFrame:
|
||||
camera: str
|
||||
frame_id: int
|
||||
raw_text: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class InferenceCase:
|
||||
issue_id: int
|
||||
issue_dir: Path
|
||||
case_dir: Path
|
||||
camera4_bin: Path
|
||||
relative_case_dir: Path
|
||||
output_dir: Path
|
||||
frame_window_source: str
|
||||
target_frame_text: str | None = None
|
||||
target_frame_id: int | None = None
|
||||
requested_frame_index_start: int | None = None
|
||||
requested_frame_index_end: int | None = None
|
||||
requested_frame_id_start: int | None = None
|
||||
requested_frame_id_end: int | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class CaseResult:
|
||||
issue_id: int
|
||||
issue_dir: str
|
||||
case_dir: str
|
||||
camera4_bin: str
|
||||
relative_case_dir: str
|
||||
output_dir: str
|
||||
status: str
|
||||
detail: str
|
||||
command: list[str]
|
||||
log_path: str | None = None
|
||||
frame_window_source: str | None = None
|
||||
target_frame_text: str | None = None
|
||||
target_frame_id: int | None = None
|
||||
requested_frame_index_start: int | None = None
|
||||
requested_frame_index_end: int | None = None
|
||||
requested_frame_id_start: int | None = None
|
||||
requested_frame_id_end: int | None = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"issue_id": self.issue_id,
|
||||
"issue_dir": self.issue_dir,
|
||||
"case_dir": self.case_dir,
|
||||
"camera4_bin": self.camera4_bin,
|
||||
"relative_case_dir": self.relative_case_dir,
|
||||
"output_dir": self.output_dir,
|
||||
"status": self.status,
|
||||
"detail": self.detail,
|
||||
"command": self.command,
|
||||
"log_path": self.log_path,
|
||||
"frame_window_source": self.frame_window_source,
|
||||
"target_frame_text": self.target_frame_text,
|
||||
"target_frame_id": self.target_frame_id,
|
||||
"requested_frame_index_start": self.requested_frame_index_start,
|
||||
"requested_frame_index_end": self.requested_frame_index_end,
|
||||
"requested_frame_id_start": self.requested_frame_id_start,
|
||||
"requested_frame_id_end": self.requested_frame_id_end,
|
||||
}
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Run exported-model inference on all downloaded issue-data camera4.bin cases."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--download-root",
|
||||
default=str(DEFAULT_DOWNLOAD_ROOT),
|
||||
help="Root directory produced by download_issue_data.py.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-root",
|
||||
default=str(DEFAULT_OUTPUT_ROOT),
|
||||
help="Root directory where inference outputs will be written.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--manifest-path",
|
||||
default="",
|
||||
help="Optional explicit manifest JSON path. Defaults to <output-root>/inference_manifest.json",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--python-bin",
|
||||
default=str(DEFAULT_PYTHON_BIN),
|
||||
help="Python interpreter used to launch run_two_roi_exported_onnx_infer.py.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--inference-script",
|
||||
default=str(DEFAULT_INFERENCE_SCRIPT),
|
||||
help="Path to run_two_roi_exported_onnx_infer.py.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--issue-json",
|
||||
default="",
|
||||
help="Optional Feishu issue export JSON used to resolve 问题发生frameid windows.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--issue-id",
|
||||
action="append",
|
||||
dest="issue_ids",
|
||||
type=int,
|
||||
help="Optional issue id filter. Can be repeated.",
|
||||
)
|
||||
parser.add_argument("--video-stride", type=int, default=1)
|
||||
parser.add_argument("--max-images", type=int, default=0)
|
||||
parser.add_argument("--frame-index-start", type=int, default=None)
|
||||
parser.add_argument("--frame-index-end", type=int, default=None)
|
||||
parser.add_argument("--frame-id-start", type=int, default=None)
|
||||
parser.add_argument("--frame-id-end", type=int, default=None)
|
||||
parser.add_argument("--target-frame-id", type=int, default=None)
|
||||
parser.add_argument("--frame-before", type=int, default=100)
|
||||
parser.add_argument("--frame-after", type=int, default=100)
|
||||
parser.add_argument("--use-issue-frame-window", action="store_true")
|
||||
parser.add_argument(
|
||||
"--missing-issue-frame-policy",
|
||||
choices=("full", "skip"),
|
||||
default="full",
|
||||
help="How to handle cases without a usable 问题发生frameid when --use-issue-frame-window is enabled.",
|
||||
)
|
||||
parser.add_argument("--exported-model", type=str, default="")
|
||||
parser.add_argument("--device", type=str, default="")
|
||||
parser.add_argument("--providers", nargs="*", default=None)
|
||||
parser.add_argument("--enable-attr", action="store_true")
|
||||
parser.add_argument("--enable-cross-class-merge-prior", action="store_true")
|
||||
parser.add_argument("--save-aggregate-predictions", action="store_true")
|
||||
parser.add_argument(
|
||||
"--inference-arg",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Extra argument forwarded to run_two_roi_exported_onnx_infer.py. Can be repeated.",
|
||||
)
|
||||
parser.add_argument("--skip-existing", action="store_true")
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
args = parser.parse_args()
|
||||
if args.frame_before < 0:
|
||||
parser.error("--frame-before must be greater than or equal to 0")
|
||||
if args.frame_after < 0:
|
||||
parser.error("--frame-after must be greater than or equal to 0")
|
||||
if (
|
||||
args.frame_index_start is not None
|
||||
and args.frame_index_end is not None
|
||||
and args.frame_index_start > args.frame_index_end
|
||||
):
|
||||
parser.error("--frame-index-start must be less than or equal to --frame-index-end")
|
||||
if (
|
||||
args.frame_id_start is not None
|
||||
and args.frame_id_end is not None
|
||||
and args.frame_id_start > args.frame_id_end
|
||||
):
|
||||
parser.error("--frame-id-start must be less than or equal to --frame-id-end")
|
||||
if args.use_issue_frame_window and not args.issue_json:
|
||||
parser.error("--issue-json is required when --use-issue-frame-window is enabled")
|
||||
return args
|
||||
|
||||
|
||||
def ensure_dir(path: Path, dry_run: bool) -> None:
|
||||
if dry_run:
|
||||
return
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def log_progress(message: str) -> None:
|
||||
timestamp = datetime.now().astimezone().strftime("%Y-%m-%d %H:%M:%S")
|
||||
print(f"[run_issue_data_inference {timestamp}] {message}", flush=True)
|
||||
|
||||
|
||||
def compact_text(value: object, max_len: int = 96) -> str:
|
||||
text = "" if value is None else str(value).strip()
|
||||
text = re.sub(r"\s+", " ", text)
|
||||
if len(text) <= max_len:
|
||||
return text
|
||||
return f"{text[: max_len - 3]}..."
|
||||
|
||||
|
||||
def parse_issue_id_from_path(path: Path) -> int | None:
|
||||
for part in path.parts:
|
||||
match = ISSUE_DIR_RE.match(part)
|
||||
if match:
|
||||
return int(match.group(1))
|
||||
return None
|
||||
|
||||
|
||||
def load_issue_items(path: Path) -> list[dict]:
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
return payload["items"]
|
||||
|
||||
|
||||
def parse_target_frame(frame_text: object) -> tuple[TargetFrame | None, str]:
|
||||
text = "" if frame_text is None else str(frame_text).strip()
|
||||
if not text:
|
||||
return None, "missing 问题发生frameid"
|
||||
camera4_match = FRAME_ID_CAMERA4_RE.search(text)
|
||||
if camera4_match:
|
||||
frame_id = int(camera4_match.group(1))
|
||||
if frame_id <= 0:
|
||||
return None, f"non-positive 问题发生frameid: {text!r}"
|
||||
return TargetFrame(camera="camera4", frame_id=frame_id, raw_text=text), ""
|
||||
if SIGNED_INTEGER_RE.fullmatch(text):
|
||||
frame_id = int(text)
|
||||
if frame_id <= 0:
|
||||
return None, f"non-positive 问题发生frameid: {text!r}"
|
||||
return TargetFrame(camera="any", frame_id=frame_id, raw_text=text), ""
|
||||
any_camera_match = FRAME_ID_ANY_CAMERA_RE.search(text)
|
||||
if any_camera_match:
|
||||
frame_id = int(any_camera_match.group(2))
|
||||
if frame_id <= 0:
|
||||
return None, f"non-positive 问题发生frameid: {text!r}"
|
||||
return TargetFrame(camera=any_camera_match.group(1).lower(), frame_id=frame_id, raw_text=text), ""
|
||||
return None, f"unparseable 问题发生frameid: {text!r}"
|
||||
|
||||
|
||||
def build_issue_item_lookup(issue_json: Path) -> dict[int, dict]:
|
||||
return {int(item["id"]): item for item in load_issue_items(issue_json)}
|
||||
|
||||
|
||||
def has_manual_frame_window(args: argparse.Namespace) -> bool:
|
||||
return any(
|
||||
value is not None
|
||||
for value in (
|
||||
args.frame_index_start,
|
||||
args.frame_index_end,
|
||||
args.frame_id_start,
|
||||
args.frame_id_end,
|
||||
args.target_frame_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def build_case_result(
|
||||
case: InferenceCase,
|
||||
*,
|
||||
status: str,
|
||||
detail: str,
|
||||
command: list[str] | None = None,
|
||||
log_path: str | None = None,
|
||||
) -> CaseResult:
|
||||
return CaseResult(
|
||||
issue_id=case.issue_id,
|
||||
issue_dir=str(case.issue_dir),
|
||||
case_dir=str(case.case_dir),
|
||||
camera4_bin=str(case.camera4_bin),
|
||||
relative_case_dir=str(case.relative_case_dir),
|
||||
output_dir=str(case.output_dir),
|
||||
status=status,
|
||||
detail=detail,
|
||||
command=command or [],
|
||||
log_path=log_path,
|
||||
frame_window_source=case.frame_window_source,
|
||||
target_frame_text=case.target_frame_text,
|
||||
target_frame_id=case.target_frame_id,
|
||||
requested_frame_index_start=case.requested_frame_index_start,
|
||||
requested_frame_index_end=case.requested_frame_index_end,
|
||||
requested_frame_id_start=case.requested_frame_id_start,
|
||||
requested_frame_id_end=case.requested_frame_id_end,
|
||||
)
|
||||
|
||||
|
||||
def discover_cases(download_root: Path, output_root: Path, issue_filter: set[int] | None) -> list[InferenceCase]:
|
||||
cases: list[InferenceCase] = []
|
||||
for camera4_bin in sorted(download_root.rglob("camera4.bin")):
|
||||
if camera4_bin.parent.name != "sigmastar.1":
|
||||
continue
|
||||
try:
|
||||
relative_camera = camera4_bin.relative_to(download_root)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
issue_id = parse_issue_id_from_path(relative_camera)
|
||||
if issue_id is None:
|
||||
continue
|
||||
if issue_filter and issue_id not in issue_filter:
|
||||
continue
|
||||
|
||||
case_dir = camera4_bin.parent.parent
|
||||
relative_case_dir = case_dir.relative_to(download_root)
|
||||
issue_dir = download_root / f"issue_{issue_id}"
|
||||
output_dir = output_root / relative_case_dir
|
||||
cases.append(
|
||||
InferenceCase(
|
||||
issue_id=issue_id,
|
||||
issue_dir=issue_dir,
|
||||
case_dir=case_dir,
|
||||
camera4_bin=camera4_bin,
|
||||
relative_case_dir=relative_case_dir,
|
||||
output_dir=output_dir,
|
||||
frame_window_source="full",
|
||||
)
|
||||
)
|
||||
return cases
|
||||
|
||||
|
||||
def apply_frame_windows(
|
||||
cases: list[InferenceCase],
|
||||
args: argparse.Namespace,
|
||||
issue_item_lookup: dict[int, dict],
|
||||
) -> tuple[list[InferenceCase], list[CaseResult]]:
|
||||
prepared_cases: list[InferenceCase] = []
|
||||
skipped_results: list[CaseResult] = []
|
||||
|
||||
for case in cases:
|
||||
prepared_case = InferenceCase(
|
||||
issue_id=case.issue_id,
|
||||
issue_dir=case.issue_dir,
|
||||
case_dir=case.case_dir,
|
||||
camera4_bin=case.camera4_bin,
|
||||
relative_case_dir=case.relative_case_dir,
|
||||
output_dir=case.output_dir,
|
||||
frame_window_source="full",
|
||||
)
|
||||
|
||||
if has_manual_frame_window(args):
|
||||
requested_frame_id_start = args.frame_id_start
|
||||
requested_frame_id_end = args.frame_id_end
|
||||
if args.target_frame_id is not None:
|
||||
if requested_frame_id_start is None:
|
||||
requested_frame_id_start = max(0, args.target_frame_id - args.frame_before)
|
||||
if requested_frame_id_end is None:
|
||||
requested_frame_id_end = args.target_frame_id + args.frame_after
|
||||
prepared_case = InferenceCase(
|
||||
issue_id=case.issue_id,
|
||||
issue_dir=case.issue_dir,
|
||||
case_dir=case.case_dir,
|
||||
camera4_bin=case.camera4_bin,
|
||||
relative_case_dir=case.relative_case_dir,
|
||||
output_dir=case.output_dir,
|
||||
frame_window_source="manual",
|
||||
target_frame_id=args.target_frame_id,
|
||||
requested_frame_index_start=args.frame_index_start,
|
||||
requested_frame_index_end=args.frame_index_end,
|
||||
requested_frame_id_start=requested_frame_id_start,
|
||||
requested_frame_id_end=requested_frame_id_end,
|
||||
)
|
||||
prepared_cases.append(prepared_case)
|
||||
continue
|
||||
|
||||
if not args.use_issue_frame_window:
|
||||
prepared_cases.append(prepared_case)
|
||||
continue
|
||||
|
||||
item = issue_item_lookup.get(case.issue_id)
|
||||
if item is None:
|
||||
if args.missing_issue_frame_policy == "skip":
|
||||
skipped_results.append(
|
||||
build_case_result(
|
||||
prepared_case,
|
||||
status="skipped_missing_issue_record",
|
||||
detail="issue id not found in issue_json",
|
||||
)
|
||||
)
|
||||
continue
|
||||
prepared_case = InferenceCase(
|
||||
issue_id=case.issue_id,
|
||||
issue_dir=case.issue_dir,
|
||||
case_dir=case.case_dir,
|
||||
camera4_bin=case.camera4_bin,
|
||||
relative_case_dir=case.relative_case_dir,
|
||||
output_dir=case.output_dir,
|
||||
frame_window_source="full_missing_issue_record",
|
||||
)
|
||||
prepared_cases.append(prepared_case)
|
||||
continue
|
||||
|
||||
target_frame, target_frame_error = parse_target_frame(item.get("问题发生frameid"))
|
||||
if target_frame is None:
|
||||
if args.missing_issue_frame_policy == "skip":
|
||||
skipped_results.append(
|
||||
build_case_result(
|
||||
prepared_case,
|
||||
status="skipped_missing_issue_frame",
|
||||
detail=target_frame_error,
|
||||
)
|
||||
)
|
||||
continue
|
||||
prepared_case = InferenceCase(
|
||||
issue_id=case.issue_id,
|
||||
issue_dir=case.issue_dir,
|
||||
case_dir=case.case_dir,
|
||||
camera4_bin=case.camera4_bin,
|
||||
relative_case_dir=case.relative_case_dir,
|
||||
output_dir=case.output_dir,
|
||||
frame_window_source=(
|
||||
"full_missing_issue_frame"
|
||||
if target_frame_error.startswith("missing ")
|
||||
else "full_invalid_issue_frame"
|
||||
),
|
||||
)
|
||||
prepared_cases.append(prepared_case)
|
||||
continue
|
||||
|
||||
if target_frame.camera not in {"camera4", "any"}:
|
||||
if args.missing_issue_frame_policy == "skip":
|
||||
skipped_results.append(
|
||||
build_case_result(
|
||||
prepared_case,
|
||||
status="skipped_unsupported_issue_camera",
|
||||
detail=f"unsupported target camera for window inference: {target_frame.camera}",
|
||||
)
|
||||
)
|
||||
continue
|
||||
prepared_case = InferenceCase(
|
||||
issue_id=case.issue_id,
|
||||
issue_dir=case.issue_dir,
|
||||
case_dir=case.case_dir,
|
||||
camera4_bin=case.camera4_bin,
|
||||
relative_case_dir=case.relative_case_dir,
|
||||
output_dir=case.output_dir,
|
||||
frame_window_source="full_unsupported_issue_camera",
|
||||
target_frame_text=target_frame.raw_text,
|
||||
)
|
||||
prepared_cases.append(prepared_case)
|
||||
continue
|
||||
|
||||
prepared_case = InferenceCase(
|
||||
issue_id=case.issue_id,
|
||||
issue_dir=case.issue_dir,
|
||||
case_dir=case.case_dir,
|
||||
camera4_bin=case.camera4_bin,
|
||||
relative_case_dir=case.relative_case_dir,
|
||||
output_dir=case.output_dir,
|
||||
frame_window_source="issue_frame",
|
||||
target_frame_text=target_frame.raw_text,
|
||||
target_frame_id=target_frame.frame_id,
|
||||
requested_frame_id_start=max(0, target_frame.frame_id - args.frame_before),
|
||||
requested_frame_id_end=target_frame.frame_id + args.frame_after,
|
||||
)
|
||||
prepared_cases.append(prepared_case)
|
||||
|
||||
return prepared_cases, skipped_results
|
||||
|
||||
|
||||
def has_existing_outputs(output_dir: Path) -> bool:
|
||||
if (output_dir / "predictions_merged.json").is_file():
|
||||
return True
|
||||
merge_json_dir = output_dir / "predictions" / "merge"
|
||||
if merge_json_dir.is_dir():
|
||||
for child in merge_json_dir.iterdir():
|
||||
if child.is_file():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def build_command(case: InferenceCase, args: argparse.Namespace) -> list[str]:
|
||||
command = [
|
||||
str(Path(args.python_bin).resolve()),
|
||||
str(Path(args.inference_script).resolve()),
|
||||
"--video-case-dir",
|
||||
str(case.camera4_bin),
|
||||
"--output-dir",
|
||||
str(case.output_dir),
|
||||
"--video-stride",
|
||||
str(args.video_stride),
|
||||
]
|
||||
if args.max_images > 0:
|
||||
command.extend(["--max-images", str(args.max_images)])
|
||||
if case.target_frame_id is not None:
|
||||
command.extend(["--target-frame-id", str(case.target_frame_id)])
|
||||
if case.requested_frame_index_start is not None:
|
||||
command.extend(["--frame-index-start", str(case.requested_frame_index_start)])
|
||||
if case.requested_frame_index_end is not None:
|
||||
command.extend(["--frame-index-end", str(case.requested_frame_index_end)])
|
||||
if case.requested_frame_id_start is not None:
|
||||
command.extend(["--frame-id-start", str(case.requested_frame_id_start)])
|
||||
if case.requested_frame_id_end is not None:
|
||||
command.extend(["--frame-id-end", str(case.requested_frame_id_end)])
|
||||
if args.exported_model:
|
||||
command.extend(["--exported-model", args.exported_model])
|
||||
if args.device:
|
||||
command.extend(["--device", args.device])
|
||||
if args.providers:
|
||||
command.extend(["--providers", *args.providers])
|
||||
if args.enable_attr:
|
||||
command.append("--enable-attr")
|
||||
if args.enable_cross_class_merge_prior:
|
||||
command.append("--enable-cross-class-merge-prior")
|
||||
if args.save_aggregate_predictions:
|
||||
command.append("--save-aggregate-predictions")
|
||||
for extra_arg in args.inference_arg:
|
||||
command.append(extra_arg)
|
||||
return command
|
||||
|
||||
|
||||
def write_case_status_files(case: InferenceCase, command: list[str], log_text: str, dry_run: bool) -> str:
|
||||
status_dir = case.output_dir / "_status"
|
||||
if dry_run:
|
||||
return str(status_dir / "inference.log")
|
||||
ensure_dir(status_dir, dry_run=False)
|
||||
(status_dir / "command.txt").write_text(" ".join(command) + "\n", encoding="utf-8")
|
||||
log_path = status_dir / "inference.log"
|
||||
log_path.write_text(log_text, encoding="utf-8")
|
||||
return str(log_path)
|
||||
|
||||
|
||||
def summarize_process_output(completed: subprocess.CompletedProcess[str]) -> str:
|
||||
stdout = completed.stdout.strip()
|
||||
stderr = completed.stderr.strip()
|
||||
if completed.returncode == 0:
|
||||
if stdout:
|
||||
return stdout.splitlines()[-1]
|
||||
return "inference completed"
|
||||
if stderr:
|
||||
return stderr.splitlines()[-1]
|
||||
if stdout:
|
||||
return stdout.splitlines()[-1]
|
||||
return f"inference failed with return code {completed.returncode}"
|
||||
|
||||
|
||||
def run_case(case: InferenceCase, args: argparse.Namespace) -> CaseResult:
|
||||
command = build_command(case, args)
|
||||
|
||||
if args.skip_existing and has_existing_outputs(case.output_dir):
|
||||
return build_case_result(
|
||||
case,
|
||||
status="skipped_existing",
|
||||
detail="existing inference outputs found",
|
||||
command=command,
|
||||
log_path=None,
|
||||
)
|
||||
|
||||
if args.dry_run:
|
||||
return build_case_result(
|
||||
case,
|
||||
status="planned",
|
||||
detail="would run inference",
|
||||
command=command,
|
||||
log_path=str(case.output_dir / "_status" / "inference.log"),
|
||||
)
|
||||
|
||||
ensure_dir(case.output_dir, dry_run=False)
|
||||
completed = subprocess.run(
|
||||
command,
|
||||
cwd=str(ROOT),
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
combined_log = "\n".join(
|
||||
part for part in (completed.stdout.strip(), completed.stderr.strip()) if part
|
||||
)
|
||||
log_path = write_case_status_files(case, command, combined_log + ("\n" if combined_log else ""), dry_run=False)
|
||||
status = "success" if completed.returncode == 0 else "failed"
|
||||
detail = summarize_process_output(completed)
|
||||
return build_case_result(
|
||||
case,
|
||||
status=status,
|
||||
detail=detail,
|
||||
command=command,
|
||||
log_path=log_path,
|
||||
)
|
||||
|
||||
|
||||
def build_manifest(
|
||||
args: argparse.Namespace,
|
||||
download_root: Path,
|
||||
output_root: Path,
|
||||
discovered_cases: list[InferenceCase],
|
||||
results: list[CaseResult],
|
||||
) -> dict:
|
||||
summary: dict[str, int] = {}
|
||||
for result in results:
|
||||
summary[result.status] = summary.get(result.status, 0) + 1
|
||||
return {
|
||||
"generated_at": datetime.now().astimezone().isoformat(timespec="seconds"),
|
||||
"download_root": str(download_root),
|
||||
"output_root": str(output_root),
|
||||
"python_bin": str(Path(args.python_bin).resolve()),
|
||||
"inference_script": str(Path(args.inference_script).resolve()),
|
||||
"dry_run": args.dry_run,
|
||||
"skip_existing": args.skip_existing,
|
||||
"issue_filter": args.issue_ids or [],
|
||||
"issue_json": args.issue_json,
|
||||
"use_issue_frame_window": args.use_issue_frame_window,
|
||||
"missing_issue_frame_policy": args.missing_issue_frame_policy,
|
||||
"video_stride": args.video_stride,
|
||||
"max_images": args.max_images,
|
||||
"frame_index_start": args.frame_index_start,
|
||||
"frame_index_end": args.frame_index_end,
|
||||
"frame_id_start": args.frame_id_start,
|
||||
"frame_id_end": args.frame_id_end,
|
||||
"target_frame_id": args.target_frame_id,
|
||||
"frame_before": args.frame_before,
|
||||
"frame_after": args.frame_after,
|
||||
"exported_model": args.exported_model,
|
||||
"device": args.device,
|
||||
"providers": args.providers or [],
|
||||
"enable_attr": args.enable_attr,
|
||||
"enable_cross_class_merge_prior": args.enable_cross_class_merge_prior,
|
||||
"save_aggregate_predictions": args.save_aggregate_predictions,
|
||||
"inference_args": args.inference_arg,
|
||||
"total_cases": len(discovered_cases),
|
||||
"summary": summary,
|
||||
"cases": [result.to_dict() for result in results],
|
||||
}
|
||||
|
||||
|
||||
def print_summary(manifest: dict) -> None:
|
||||
print(f"download_root: {manifest['download_root']}")
|
||||
print(f"output_root: {manifest['output_root']}")
|
||||
print(f"dry_run: {manifest['dry_run']}")
|
||||
if manifest["use_issue_frame_window"]:
|
||||
print(
|
||||
"issue_frame_window: "
|
||||
f"enabled issue_json={manifest['issue_json']} "
|
||||
f"before={manifest['frame_before']} after={manifest['frame_after']} "
|
||||
f"missing_policy={manifest['missing_issue_frame_policy']}"
|
||||
)
|
||||
elif (
|
||||
manifest["target_frame_id"] is not None
|
||||
or manifest["frame_id_start"] is not None
|
||||
or manifest["frame_id_end"] is not None
|
||||
or manifest["frame_index_start"] is not None
|
||||
or manifest["frame_index_end"] is not None
|
||||
):
|
||||
print(
|
||||
"manual_frame_window: "
|
||||
f"target_frame_id={manifest['target_frame_id']} "
|
||||
f"frame_id=[{manifest['frame_id_start'] if manifest['frame_id_start'] is not None else '-inf'}, "
|
||||
f"{manifest['frame_id_end'] if manifest['frame_id_end'] is not None else '+inf'}] "
|
||||
f"frame_index=[{manifest['frame_index_start'] if manifest['frame_index_start'] is not None else '-inf'}, "
|
||||
f"{manifest['frame_index_end'] if manifest['frame_index_end'] is not None else '+inf'}]"
|
||||
)
|
||||
print(f"total_cases: {manifest['total_cases']}")
|
||||
for status, count in sorted(manifest["summary"].items()):
|
||||
print(f"{status}: {count}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
download_root = Path(args.download_root).resolve()
|
||||
output_root = Path(args.output_root).resolve()
|
||||
manifest_path = (
|
||||
Path(args.manifest_path).resolve()
|
||||
if args.manifest_path
|
||||
else output_root / "inference_manifest.json"
|
||||
)
|
||||
|
||||
issue_filter = set(args.issue_ids) if args.issue_ids else None
|
||||
cases = discover_cases(download_root, output_root, issue_filter)
|
||||
if not cases:
|
||||
raise FileNotFoundError(f"No */sigmastar.1/camera4.bin cases found under {download_root}")
|
||||
|
||||
issue_item_lookup: dict[int, dict] = {}
|
||||
if args.use_issue_frame_window:
|
||||
issue_item_lookup = build_issue_item_lookup(Path(args.issue_json).resolve())
|
||||
|
||||
prepared_cases, skipped_results = apply_frame_windows(cases, args, issue_item_lookup)
|
||||
log_progress(
|
||||
"cases discovered: "
|
||||
f"total={len(cases)} runnable={len(prepared_cases)} skipped_precheck={len(skipped_results)}"
|
||||
)
|
||||
for skipped_result in skipped_results:
|
||||
log_progress(
|
||||
f"skip issue_{skipped_result.issue_id} {compact_text(skipped_result.relative_case_dir)}: "
|
||||
f"{skipped_result.status} ({compact_text(skipped_result.detail, max_len=72)})"
|
||||
)
|
||||
|
||||
ensure_dir(output_root, dry_run=args.dry_run)
|
||||
results = list(skipped_results)
|
||||
total_prepared_cases = len(prepared_cases)
|
||||
for index, case in enumerate(prepared_cases, start=1):
|
||||
log_progress(
|
||||
f"[{index}/{total_prepared_cases}] issue_{case.issue_id} "
|
||||
f"{compact_text(case.relative_case_dir)}: start (window={case.frame_window_source})"
|
||||
)
|
||||
case_result = run_case(case, args)
|
||||
results.append(case_result)
|
||||
log_progress(
|
||||
f"[{index}/{total_prepared_cases}] issue_{case.issue_id} "
|
||||
f"{compact_text(case.relative_case_dir)}: "
|
||||
f"{case_result.status} ({compact_text(case_result.detail, max_len=72)})"
|
||||
)
|
||||
manifest = build_manifest(args, download_root, output_root, cases, results)
|
||||
|
||||
if not args.dry_run:
|
||||
ensure_dir(manifest_path.parent, dry_run=False)
|
||||
manifest_path.write_text(
|
||||
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
print_summary(manifest)
|
||||
if args.dry_run:
|
||||
print(f"manifest (not written in dry-run): {manifest_path}")
|
||||
else:
|
||||
print(f"manifest: {manifest_path}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
193
tools/feishu_project/run_issue_data_inference.sh
Executable file
193
tools/feishu_project/run_issue_data_inference.sh
Executable file
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
PROJECT_ROOT=$(cd "${SCRIPT_DIR}/../.." && pwd)
|
||||
|
||||
DOWNLOAD_ROOT=${DOWNLOAD_ROOT:-/data1/dongying/Mono3d/G1Q3/feishu_project/downloaded_issue_data}
|
||||
OUTPUT_ROOT=${OUTPUT_ROOT:-/data1/dongying/Mono3d/G1Q3/feishu_project/inference_issue_data}
|
||||
MANIFEST_PATH=${MANIFEST_PATH:-"${OUTPUT_ROOT}/inference_manifest.json"}
|
||||
PYTHON_BIN=${PYTHON_BIN:-/deeplearning_team/ydong/dongying/miniconda/envs/dev/bin/python}
|
||||
INFERENCE_SCRIPT=${INFERENCE_SCRIPT:-"${PROJECT_ROOT}/tools/model_inference/core/run_two_roi_exported_onnx_infer.py"}
|
||||
ISSUE_TRACKING_SCRIPT=${ISSUE_TRACKING_SCRIPT:-"${PROJECT_ROOT}/tools/feishu_project/run_issue_data_tracking.sh"}
|
||||
ISSUE_JSON=${ISSUE_JSON:-}
|
||||
VIDEO_STRIDE=${VIDEO_STRIDE:-1}
|
||||
MAX_IMAGES=${MAX_IMAGES:-0}
|
||||
FRAME_INDEX_START=${FRAME_INDEX_START:-}
|
||||
FRAME_INDEX_END=${FRAME_INDEX_END:-}
|
||||
FRAME_ID_START=${FRAME_ID_START:-}
|
||||
FRAME_ID_END=${FRAME_ID_END:-}
|
||||
TARGET_FRAME_ID=${TARGET_FRAME_ID:-}
|
||||
FRAME_BEFORE=${FRAME_BEFORE:-100}
|
||||
FRAME_AFTER=${FRAME_AFTER:-100}
|
||||
USE_ISSUE_FRAME_WINDOW=${USE_ISSUE_FRAME_WINDOW:-0}
|
||||
MISSING_ISSUE_FRAME_POLICY=${MISSING_ISSUE_FRAME_POLICY:-full}
|
||||
EXPORTED_MODEL=${EXPORTED_MODEL:-${PROJECT_ROOT}/runs/export/train_mono3d_two_roi_20260416-raw_no_edge/merged_model.torchscript}
|
||||
DEVICE=${DEVICE:-}
|
||||
PROVIDERS=${PROVIDERS:-}
|
||||
ENABLE_ATTR=${ENABLE_ATTR:-0}
|
||||
ENABLE_CROSS_CLASS_MERGE_PRIOR=${ENABLE_CROSS_CLASS_MERGE_PRIOR:-1}
|
||||
SAVE_AGGREGATE_PREDICTIONS=${SAVE_AGGREGATE_PREDICTIONS:-1}
|
||||
SKIP_EXISTING=${SKIP_EXISTING:-0}
|
||||
DRY_RUN=${DRY_RUN:-0}
|
||||
ENABLE_TRACKING=${ENABLE_TRACKING:-0}
|
||||
INFERENCE_EXTRA_ARGS=${INFERENCE_EXTRA_ARGS:-}
|
||||
|
||||
resolve_tracking_model_version() {
|
||||
if [[ -n "${TRACKING_MODEL_VERSION:-}" ]]; then
|
||||
printf '%s\n' "${TRACKING_MODEL_VERSION}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -n "${EXPORTED_MODEL}" ]] && [[ "${EXPORTED_MODEL}" =~ ([0-9]{8}) ]]; then
|
||||
printf '%s\n' "${BASH_REMATCH[1]}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
FORWARD_ARGS=()
|
||||
ISSUE_ID_ARGS=()
|
||||
while (($# > 0)); do
|
||||
case "$1" in
|
||||
--issue-id)
|
||||
if (($# < 2)); then
|
||||
echo "Error: --issue-id requires a value" >&2
|
||||
exit 1
|
||||
fi
|
||||
ISSUE_ID_ARGS+=("$1" "$2")
|
||||
FORWARD_ARGS+=("$1" "$2")
|
||||
shift 2
|
||||
;;
|
||||
--issue-id=*)
|
||||
ISSUE_ID_ARGS+=("$1")
|
||||
FORWARD_ARGS+=("$1")
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
FORWARD_ARGS+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
CMD=(
|
||||
"${PYTHON_BIN}" "${PROJECT_ROOT}/tools/feishu_project/run_issue_data_inference.py"
|
||||
--download-root "${DOWNLOAD_ROOT}"
|
||||
--output-root "${OUTPUT_ROOT}"
|
||||
--manifest-path "${MANIFEST_PATH}"
|
||||
--python-bin "${PYTHON_BIN}"
|
||||
--inference-script "${INFERENCE_SCRIPT}"
|
||||
--video-stride "${VIDEO_STRIDE}"
|
||||
)
|
||||
|
||||
if [[ -n "${ISSUE_JSON}" ]]; then
|
||||
CMD+=(--issue-json "${ISSUE_JSON}")
|
||||
fi
|
||||
|
||||
if [[ "${MAX_IMAGES}" != "0" ]]; then
|
||||
CMD+=(--max-images "${MAX_IMAGES}")
|
||||
fi
|
||||
|
||||
if [[ -n "${FRAME_INDEX_START}" ]]; then
|
||||
CMD+=(--frame-index-start "${FRAME_INDEX_START}")
|
||||
fi
|
||||
|
||||
if [[ -n "${FRAME_INDEX_END}" ]]; then
|
||||
CMD+=(--frame-index-end "${FRAME_INDEX_END}")
|
||||
fi
|
||||
|
||||
if [[ -n "${FRAME_ID_START}" ]]; then
|
||||
CMD+=(--frame-id-start "${FRAME_ID_START}")
|
||||
fi
|
||||
|
||||
if [[ -n "${FRAME_ID_END}" ]]; then
|
||||
CMD+=(--frame-id-end "${FRAME_ID_END}")
|
||||
fi
|
||||
|
||||
if [[ -n "${TARGET_FRAME_ID}" ]]; then
|
||||
CMD+=(--target-frame-id "${TARGET_FRAME_ID}")
|
||||
fi
|
||||
|
||||
if [[ "${USE_ISSUE_FRAME_WINDOW}" != "1" && "${FRAME_BEFORE}" != "100" ]]; then
|
||||
CMD+=(--frame-before "${FRAME_BEFORE}")
|
||||
fi
|
||||
|
||||
if [[ "${USE_ISSUE_FRAME_WINDOW}" != "1" && "${FRAME_AFTER}" != "100" ]]; then
|
||||
CMD+=(--frame-after "${FRAME_AFTER}")
|
||||
fi
|
||||
|
||||
if [[ "${USE_ISSUE_FRAME_WINDOW}" == "1" ]]; then
|
||||
CMD+=(--use-issue-frame-window --frame-before "${FRAME_BEFORE}" --frame-after "${FRAME_AFTER}" --missing-issue-frame-policy "${MISSING_ISSUE_FRAME_POLICY}")
|
||||
fi
|
||||
|
||||
if [[ -n "${EXPORTED_MODEL}" ]]; then
|
||||
CMD+=(--exported-model "${EXPORTED_MODEL}")
|
||||
fi
|
||||
|
||||
if [[ -n "${DEVICE}" ]]; then
|
||||
CMD+=(--device "${DEVICE}")
|
||||
fi
|
||||
|
||||
if [[ -n "${PROVIDERS}" ]]; then
|
||||
# shellcheck disable=SC2206
|
||||
PROVIDER_ARR=(${PROVIDERS})
|
||||
CMD+=(--providers "${PROVIDER_ARR[@]}")
|
||||
fi
|
||||
|
||||
if [[ "${ENABLE_ATTR}" == "1" ]]; then
|
||||
CMD+=(--enable-attr)
|
||||
fi
|
||||
|
||||
if [[ "${ENABLE_CROSS_CLASS_MERGE_PRIOR}" == "1" ]]; then
|
||||
CMD+=(--enable-cross-class-merge-prior)
|
||||
fi
|
||||
|
||||
if [[ "${SAVE_AGGREGATE_PREDICTIONS}" == "1" ]]; then
|
||||
CMD+=(--save-aggregate-predictions)
|
||||
fi
|
||||
|
||||
if [[ "${SKIP_EXISTING}" == "1" ]]; then
|
||||
CMD+=(--skip-existing)
|
||||
fi
|
||||
|
||||
if [[ "${DRY_RUN}" == "1" ]]; then
|
||||
CMD+=(--dry-run)
|
||||
fi
|
||||
|
||||
if [[ -n "${INFERENCE_EXTRA_ARGS}" ]]; then
|
||||
# shellcheck disable=SC2206
|
||||
EXTRA_ARR=(${INFERENCE_EXTRA_ARGS})
|
||||
for arg in "${EXTRA_ARR[@]}"; do
|
||||
CMD+=("--inference-arg=${arg}")
|
||||
done
|
||||
fi
|
||||
|
||||
CMD+=("${FORWARD_ARGS[@]}")
|
||||
"${CMD[@]}"
|
||||
|
||||
if [[ "${ENABLE_TRACKING}" != "1" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${DRY_RUN}" == "1" ]]; then
|
||||
echo "Skipping tracking because DRY_RUN=1"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
TRACKING_MODEL_VERSION_RESOLVED=${TRACKING_MODEL_VERSION:-}
|
||||
if [[ -z "${TRACKING_MODEL_VERSION_RESOLVED}" ]]; then
|
||||
TRACKING_MODEL_VERSION_RESOLVED=$(resolve_tracking_model_version || true)
|
||||
fi
|
||||
|
||||
if [[ -n "${TRACKING_MODEL_VERSION_RESOLVED}" ]]; then
|
||||
echo "Running tracking with model version: ${TRACKING_MODEL_VERSION_RESOLVED}"
|
||||
TRACKING_MODEL_VERSION="${TRACKING_MODEL_VERSION_RESOLVED}" \
|
||||
PYTHON_BIN="${PYTHON_BIN}" \
|
||||
bash "${ISSUE_TRACKING_SCRIPT}" "${OUTPUT_ROOT}" "${ISSUE_ID_ARGS[@]}"
|
||||
else
|
||||
echo "Running tracking without an explicit model version override"
|
||||
PYTHON_BIN="${PYTHON_BIN}" \
|
||||
bash "${ISSUE_TRACKING_SCRIPT}" "${OUTPUT_ROOT}" "${ISSUE_ID_ARGS[@]}"
|
||||
fi
|
||||
360
tools/feishu_project/run_issue_data_temporal_observe.sh
Executable file
360
tools/feishu_project/run_issue_data_temporal_observe.sh
Executable file
@@ -0,0 +1,360 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
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/feishu_project/inference_issue_data}
|
||||
OUTPUT_ROOT=${OUTPUT_ROOT:-}
|
||||
OUTPUT_DIR_NAME=${OUTPUT_DIR_NAME:-temporal_observation}
|
||||
PYTHON_BIN=${PYTHON_BIN:-/deeplearning_team/ydong/dongying/miniconda/envs/dev/bin/python}
|
||||
TEMPORAL_SCRIPT=${TEMPORAL_SCRIPT:-"${PROJECT_ROOT}/tools/temporal_analysis/evaluate_temporal_stability.py"}
|
||||
TRACKING_JSON_NAME=${TRACKING_JSON_NAME:-merge.json}
|
||||
TEMPORAL_MIN_LENGTH=${TEMPORAL_MIN_LENGTH:-3}
|
||||
TEMPORAL_CLASS_ID=${TEMPORAL_CLASS_ID:-}
|
||||
TEMPORAL_TRACK_IDS=${TEMPORAL_TRACK_IDS:-4}
|
||||
TEMPORAL_FRAME_ID_START=${TEMPORAL_FRAME_ID_START:-86421}
|
||||
TEMPORAL_FRAME_ID_END=${TEMPORAL_FRAME_ID_END:-86441}
|
||||
TEMPORAL_PLOTS=${TEMPORAL_PLOTS:-0}
|
||||
TEMPORAL_EXPORT_SERIES=${TEMPORAL_EXPORT_SERIES:-1}
|
||||
TEMPORAL_FOCUS_TRACK_PLOTS=${TEMPORAL_FOCUS_TRACK_PLOTS:-1}
|
||||
TEMPORAL_PREFER_EGO=${TEMPORAL_PREFER_EGO:-1}
|
||||
TEMPORAL_X_AXIS=${TEMPORAL_X_AXIS:-frame_id}
|
||||
TEMPORAL_HEADING_SOURCE=${TEMPORAL_HEADING_SOURCE:-camera_reg}
|
||||
|
||||
TARGET_PATH="/data1/dongying/Mono3d/G1Q3/feishu_project/inference_issue_data/issue_6974203002/pdcl_01/20260417112730/2026-04-17_11-29-29_voice-HMI二轮车位置不正确"
|
||||
ISSUE_IDS=()
|
||||
CLI_TRACK_IDS=()
|
||||
CLI_CLASS_ID=""
|
||||
CLI_FRAME_ID_START=""
|
||||
CLI_FRAME_ID_END=""
|
||||
|
||||
while (($# > 0)); do
|
||||
case "$1" in
|
||||
--issue-id)
|
||||
if (($# < 2)); then
|
||||
echo "Error: --issue-id requires a value" >&2
|
||||
exit 1
|
||||
fi
|
||||
ISSUE_IDS+=("$2")
|
||||
shift 2
|
||||
;;
|
||||
--issue-id=*)
|
||||
ISSUE_IDS+=("${1#*=}")
|
||||
shift
|
||||
;;
|
||||
--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
|
||||
;;
|
||||
--frame-id-start)
|
||||
if (($# < 2)); then
|
||||
echo "Error: --frame-id-start requires a value" >&2
|
||||
exit 1
|
||||
fi
|
||||
CLI_FRAME_ID_START="$2"
|
||||
shift 2
|
||||
;;
|
||||
--frame-id-start=*)
|
||||
CLI_FRAME_ID_START="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--frame-id-end)
|
||||
if (($# < 2)); then
|
||||
echo "Error: --frame-id-end requires a value" >&2
|
||||
exit 1
|
||||
fi
|
||||
CLI_FRAME_ID_END="$2"
|
||||
shift 2
|
||||
;;
|
||||
--frame-id-end=*)
|
||||
CLI_FRAME_ID_END="${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
|
||||
|
||||
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}")"
|
||||
|
||||
is_case_dir() {
|
||||
local dir_path="$1"
|
||||
[[ -d "${dir_path}" ]] && [[ -f "${dir_path}/${TRACKING_JSON_NAME}" ]]
|
||||
}
|
||||
|
||||
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}"
|
||||
}
|
||||
|
||||
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 "${TEMPORAL_TRACK_IDS}" ]]; then
|
||||
# shellcheck disable=SC2206
|
||||
local track_id_arr=(${TEMPORAL_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 output_dir
|
||||
local cmd
|
||||
local track_id_args
|
||||
local class_id_value=""
|
||||
local frame_id_start_value=""
|
||||
local frame_id_end_value=""
|
||||
|
||||
if [[ ! -f "${tracking_json}" ]]; then
|
||||
echo "[ERROR] ${TRACKING_JSON_NAME} not found in case directory: ${case_dir}" >&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 "${TEMPORAL_CLASS_ID}" ]]; then
|
||||
class_id_value="${TEMPORAL_CLASS_ID}"
|
||||
fi
|
||||
|
||||
if [[ -n "${CLI_FRAME_ID_START}" ]]; then
|
||||
frame_id_start_value="${CLI_FRAME_ID_START}"
|
||||
elif [[ -n "${TEMPORAL_FRAME_ID_START}" ]]; then
|
||||
frame_id_start_value="${TEMPORAL_FRAME_ID_START}"
|
||||
fi
|
||||
|
||||
if [[ -n "${CLI_FRAME_ID_END}" ]]; then
|
||||
frame_id_end_value="${CLI_FRAME_ID_END}"
|
||||
elif [[ -n "${TEMPORAL_FRAME_ID_END}" ]]; then
|
||||
frame_id_end_value="${TEMPORAL_FRAME_ID_END}"
|
||||
fi
|
||||
|
||||
cmd=(
|
||||
"${PYTHON_BIN}" "${TEMPORAL_SCRIPT}"
|
||||
--input "${tracking_json}"
|
||||
--output "${output_dir}/stability_report.json"
|
||||
--min-length "${TEMPORAL_MIN_LENGTH}"
|
||||
--x-axis "${TEMPORAL_X_AXIS}"
|
||||
--heading-source "${TEMPORAL_HEADING_SOURCE}"
|
||||
)
|
||||
|
||||
if [[ -n "${class_id_value}" ]]; then
|
||||
cmd+=(--class-id "${class_id_value}")
|
||||
fi
|
||||
|
||||
if [[ -n "${frame_id_start_value}" ]]; then
|
||||
cmd+=(--frame-id-start "${frame_id_start_value}")
|
||||
fi
|
||||
|
||||
if [[ -n "${frame_id_end_value}" ]]; then
|
||||
cmd+=(--frame-id-end "${frame_id_end_value}")
|
||||
fi
|
||||
|
||||
if [[ "${#track_id_args[@]}" -gt 0 ]]; then
|
||||
cmd+=("${track_id_args[@]}")
|
||||
fi
|
||||
|
||||
if [[ "${TEMPORAL_PLOTS}" == "1" ]]; then
|
||||
cmd+=(--plots)
|
||||
fi
|
||||
|
||||
if [[ "${TEMPORAL_EXPORT_SERIES}" == "1" ]]; then
|
||||
cmd+=(--export-series)
|
||||
fi
|
||||
|
||||
if [[ "${TEMPORAL_FOCUS_TRACK_PLOTS}" == "1" ]]; then
|
||||
cmd+=(--focus-track-plots)
|
||||
fi
|
||||
|
||||
if [[ "${TEMPORAL_PREFER_EGO}" == "1" ]]; then
|
||||
cmd+=(--prefer-ego)
|
||||
else
|
||||
cmd+=(--no-prefer-ego)
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "######################################################################"
|
||||
echo "# Feishu issue-data temporal observation"
|
||||
echo "######################################################################"
|
||||
echo "Case : ${case_dir}"
|
||||
echo "Track : ${tracking_json}"
|
||||
echo "Output : ${output_dir}"
|
||||
if [[ -n "${class_id_value}" ]]; then
|
||||
echo "Class : ${class_id_value}"
|
||||
fi
|
||||
if [[ -n "${frame_id_start_value}" || -n "${frame_id_end_value}" ]]; then
|
||||
echo "Frame ID Range: [${frame_id_start_value:-"-inf"}, ${frame_id_end_value:-"+inf"}]"
|
||||
fi
|
||||
echo "Heading Source: ${TEMPORAL_HEADING_SOURCE}"
|
||||
if [[ "${#CLI_TRACK_IDS[@]}" -gt 0 ]]; then
|
||||
echo "Track IDs: ${CLI_TRACK_IDS[*]}"
|
||||
elif [[ -n "${TEMPORAL_TRACK_IDS}" ]]; then
|
||||
echo "Track IDs: ${TEMPORAL_TRACK_IDS}"
|
||||
fi
|
||||
|
||||
"${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 [[ "${#ISSUE_IDS[@]}" -eq 0 ]]; then
|
||||
if is_case_dir "${TARGET_PATH}"; then
|
||||
run_single_case "${TARGET_PATH}"
|
||||
elif [[ -d "${TARGET_PATH}" ]]; then
|
||||
run_batch_root "${TARGET_PATH}"
|
||||
else
|
||||
echo "Error: unsupported target path: ${TARGET_PATH}" >&2
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
total_issues=0
|
||||
success_issues=0
|
||||
failed_issues=0
|
||||
|
||||
for issue_id in "${ISSUE_IDS[@]}"; do
|
||||
issue_root="${TARGET_PATH}/issue_${issue_id}"
|
||||
((total_issues += 1))
|
||||
|
||||
if [[ ! -d "${issue_root}" ]]; then
|
||||
echo "[FAIL] issue_${issue_id}: not found under ${TARGET_PATH}" >&2
|
||||
((failed_issues += 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
if run_batch_root "${issue_root}"; then
|
||||
((success_issues += 1))
|
||||
else
|
||||
((failed_issues += 1))
|
||||
echo "[FAIL] issue_${issue_id}: temporal observation failed" >&2
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
printf '[DONE] issues=%d success=%d failed=%d\n' \
|
||||
"${total_issues}" "${success_issues}" "${failed_issues}"
|
||||
|
||||
[[ "${failed_issues}" -eq 0 ]]
|
||||
347
tools/feishu_project/run_issue_data_tracking.sh
Executable file
347
tools/feishu_project/run_issue_data_tracking.sh
Executable file
@@ -0,0 +1,347 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
PROJECT_ROOT=$(cd "${SCRIPT_DIR}/../.." && pwd)
|
||||
|
||||
RESULTS_ROOT=${RESULTS_ROOT:-/data1/dongying/Mono3d/G1Q3/feishu_project/inference_issue_data}
|
||||
PYTHON_BIN=${PYTHON_BIN:-/deeplearning_team/ydong/dongying/miniconda/envs/dev/bin/python}
|
||||
TRACKING_LAUNCHER=${TRACKING_LAUNCHER:-"${PROJECT_ROOT}/tools/temporal_analysis/track_objects_exported_onnx_infer_case.sh"}
|
||||
ISSUE_CONVERT_SCRIPT=${ISSUE_CONVERT_SCRIPT:-"${PROJECT_ROOT}/tools/feishu_project/run_issue_data_convert_tracking.sh"}
|
||||
ISSUE_VISUALIZE_SCRIPT=${ISSUE_VISUALIZE_SCRIPT:-"${PROJECT_ROOT}/tools/feishu_project/run_issue_data_visualize_tracking.sh"}
|
||||
ISSUE_TEMPORAL_SCRIPT=${ISSUE_TEMPORAL_SCRIPT:-"${PROJECT_ROOT}/tools/feishu_project/run_issue_data_temporal_observe.sh"}
|
||||
TRACK_CLASSES=${TRACK_CLASSES:-0 1 2 3 4 5 6 7 8 9 10 11 12 17 18 19}
|
||||
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:-}
|
||||
MERGE_OUTPUT_NAME=${MERGE_OUTPUT_NAME:-combined_tracking.json}
|
||||
FILE_PATTERN=${FILE_PATTERN:-*.json}
|
||||
ENABLE_USE_3D=${ENABLE_USE_3D:-0}
|
||||
ENABLE_CONVERT=${ENABLE_CONVERT:-0}
|
||||
ENABLE_VISUALIZE=${ENABLE_VISUALIZE:-1}
|
||||
ENABLE_TEMPORAL_ANALYSIS=${ENABLE_TEMPORAL_ANALYSIS:-0}
|
||||
CONVERT_OUTPUT_DIR_NAME=${CONVERT_OUTPUT_DIR_NAME:-objectlist}
|
||||
CONVERT_TRACKING_JSON_NAME=${CONVERT_TRACKING_JSON_NAME:-merge.json}
|
||||
CONVERT_CAM_ID=${CONVERT_CAM_ID:-}
|
||||
VIS_DOWNLOAD_ROOT=${VIS_DOWNLOAD_ROOT:-/data1/dongying/Mono3d/G1Q3/feishu_project/downloaded_issue_data}
|
||||
VIS_OUTPUT_ROOT=${VIS_OUTPUT_ROOT:-}
|
||||
VIS_OUTPUT_DIR_NAME=${VIS_OUTPUT_DIR_NAME:-tracking_vis_raw}
|
||||
VIS_TRACKING_JSON_NAME=${VIS_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}
|
||||
TEMPORAL_OUTPUT_ROOT=${TEMPORAL_OUTPUT_ROOT:-}
|
||||
TEMPORAL_OUTPUT_DIR_NAME=${TEMPORAL_OUTPUT_DIR_NAME:-temporal_observation}
|
||||
TEMPORAL_JSON_NAME=${TEMPORAL_JSON_NAME:-merge.json}
|
||||
TEMPORAL_MIN_LENGTH=${TEMPORAL_MIN_LENGTH:-3}
|
||||
TEMPORAL_TRACK_IDS=${TEMPORAL_TRACK_IDS:-}
|
||||
TEMPORAL_CLASS_ID=${TEMPORAL_CLASS_ID:-}
|
||||
TEMPORAL_FRAME_ID_START=${TEMPORAL_FRAME_ID_START:-}
|
||||
TEMPORAL_FRAME_ID_END=${TEMPORAL_FRAME_ID_END:-}
|
||||
TEMPORAL_PLOTS=${TEMPORAL_PLOTS:-0}
|
||||
TEMPORAL_EXPORT_SERIES=${TEMPORAL_EXPORT_SERIES:-1}
|
||||
TEMPORAL_FOCUS_TRACK_PLOTS=${TEMPORAL_FOCUS_TRACK_PLOTS:-1}
|
||||
TEMPORAL_PREFER_EGO=${TEMPORAL_PREFER_EGO:-1}
|
||||
TEMPORAL_X_AXIS=${TEMPORAL_X_AXIS:-frame_id}
|
||||
TEMPORAL_HEADING_SOURCE=${TEMPORAL_HEADING_SOURCE:-camera_reg}
|
||||
TRACKING_MODEL_VERSION=${TRACKING_MODEL_VERSION:-20260416}
|
||||
|
||||
TARGET_PATH=${TARGET_PATH:-}
|
||||
ISSUE_IDS=()
|
||||
|
||||
while (($# > 0)); do
|
||||
case "$1" in
|
||||
--issue-id)
|
||||
if (($# < 2)); then
|
||||
echo "Error: --issue-id requires a value" >&2
|
||||
exit 1
|
||||
fi
|
||||
ISSUE_IDS+=("$2")
|
||||
shift 2
|
||||
;;
|
||||
--issue-id=*)
|
||||
ISSUE_IDS+=("${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 [[ ! -d "${TARGET_PATH}" ]]; then
|
||||
echo "Error: target directory does not exist: ${TARGET_PATH}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
run_tracking_wrapper() {
|
||||
local target_path="$1"
|
||||
|
||||
echo ""
|
||||
echo "######################################################################"
|
||||
echo "# Feishu issue-data tracking"
|
||||
echo "######################################################################"
|
||||
echo "Target path: ${target_path}"
|
||||
if [[ -n "${TRACKING_MODEL_VERSION}" ]]; then
|
||||
echo "Model version: ${TRACKING_MODEL_VERSION}"
|
||||
fi
|
||||
|
||||
if [[ -n "${TRACKING_MODEL_VERSION}" ]]; then
|
||||
MODEL_VERSION="${TRACKING_MODEL_VERSION}" \
|
||||
PYTHON_BIN="${PYTHON_BIN}" \
|
||||
RESULTS_ROOT="${target_path}" \
|
||||
TRACK_CLASSES="${TRACK_CLASSES}" \
|
||||
IOU_THRESH="${IOU_THRESH}" \
|
||||
MAX_AGE="${MAX_AGE}" \
|
||||
MIN_HITS="${MIN_HITS}" \
|
||||
DIST_THRESH="${DIST_THRESH}" \
|
||||
MAX_3D_DISTANCE="${MAX_3D_DISTANCE}" \
|
||||
MAX_FRAMES="${MAX_FRAMES}" \
|
||||
MERGE_OUTPUT_NAME="${MERGE_OUTPUT_NAME}" \
|
||||
FILE_PATTERN="${FILE_PATTERN}" \
|
||||
ENABLE_USE_3D="${ENABLE_USE_3D}" \
|
||||
bash "${TRACKING_LAUNCHER}" "${target_path}"
|
||||
return
|
||||
fi
|
||||
|
||||
PYTHON_BIN="${PYTHON_BIN}" \
|
||||
RESULTS_ROOT="${target_path}" \
|
||||
TRACK_CLASSES="${TRACK_CLASSES}" \
|
||||
IOU_THRESH="${IOU_THRESH}" \
|
||||
MAX_AGE="${MAX_AGE}" \
|
||||
MIN_HITS="${MIN_HITS}" \
|
||||
DIST_THRESH="${DIST_THRESH}" \
|
||||
MAX_3D_DISTANCE="${MAX_3D_DISTANCE}" \
|
||||
MAX_FRAMES="${MAX_FRAMES}" \
|
||||
MERGE_OUTPUT_NAME="${MERGE_OUTPUT_NAME}" \
|
||||
FILE_PATTERN="${FILE_PATTERN}" \
|
||||
ENABLE_USE_3D="${ENABLE_USE_3D}" \
|
||||
bash "${TRACKING_LAUNCHER}" "${target_path}"
|
||||
}
|
||||
|
||||
run_conversion_wrapper() {
|
||||
local target_path="$1"
|
||||
local convert_args=("${target_path}")
|
||||
|
||||
for issue_id in "${ISSUE_IDS[@]}"; do
|
||||
convert_args+=(--issue-id "${issue_id}")
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "######################################################################"
|
||||
echo "# Feishu issue-data protocol conversion"
|
||||
echo "######################################################################"
|
||||
echo "Target path : ${target_path}"
|
||||
echo "Tracking JSON : ${CONVERT_TRACKING_JSON_NAME}"
|
||||
echo "Output dir name : ${CONVERT_OUTPUT_DIR_NAME}"
|
||||
|
||||
if [[ -n "${CONVERT_CAM_ID}" ]]; then
|
||||
echo "Camera ID override: ${CONVERT_CAM_ID}"
|
||||
fi
|
||||
|
||||
PYTHON_BIN="${PYTHON_BIN}" \
|
||||
RESULTS_ROOT="${RESULTS_ROOT}" \
|
||||
OUTPUT_DIR_NAME="${CONVERT_OUTPUT_DIR_NAME}" \
|
||||
MERGE_JSON_NAME="${CONVERT_TRACKING_JSON_NAME}" \
|
||||
CAM_ID="${CONVERT_CAM_ID}" \
|
||||
bash "${ISSUE_CONVERT_SCRIPT}" "${convert_args[@]}"
|
||||
}
|
||||
|
||||
run_visualization_wrapper() {
|
||||
local target_path="$1"
|
||||
local visualize_args=("${target_path}")
|
||||
|
||||
for issue_id in "${ISSUE_IDS[@]}"; do
|
||||
visualize_args+=(--issue-id "${issue_id}")
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "######################################################################"
|
||||
echo "# Feishu issue-data raw-image visualization"
|
||||
echo "######################################################################"
|
||||
echo "Target path : ${target_path}"
|
||||
echo "Download root : ${VIS_DOWNLOAD_ROOT}"
|
||||
if [[ -n "${VIS_OUTPUT_ROOT}" ]]; then
|
||||
echo "Output root : ${VIS_OUTPUT_ROOT}"
|
||||
else
|
||||
echo "Output dir name: ${VIS_OUTPUT_DIR_NAME}"
|
||||
fi
|
||||
|
||||
PYTHON_BIN="${PYTHON_BIN}" \
|
||||
RESULTS_ROOT="${RESULTS_ROOT}" \
|
||||
DOWNLOAD_ROOT="${VIS_DOWNLOAD_ROOT}" \
|
||||
OUTPUT_ROOT="${VIS_OUTPUT_ROOT}" \
|
||||
OUTPUT_DIR_NAME="${VIS_OUTPUT_DIR_NAME}" \
|
||||
TRACKING_JSON_NAME="${VIS_TRACKING_JSON_NAME}" \
|
||||
VIS_MAX_FRAMES="${VIS_MAX_FRAMES}" \
|
||||
VIS_CLASS_ID="${VIS_CLASS_ID}" \
|
||||
VIS_TRACK_IDS="${VIS_TRACK_IDS}" \
|
||||
VIS_SHOW_TRAJECTORY="${VIS_SHOW_TRAJECTORY}" \
|
||||
bash "${ISSUE_VISUALIZE_SCRIPT}" "${visualize_args[@]}"
|
||||
}
|
||||
|
||||
run_temporal_wrapper() {
|
||||
local target_path="$1"
|
||||
local temporal_args=("${target_path}")
|
||||
|
||||
for issue_id in "${ISSUE_IDS[@]}"; do
|
||||
temporal_args+=(--issue-id "${issue_id}")
|
||||
done
|
||||
|
||||
if [[ -n "${TEMPORAL_CLASS_ID}" ]]; then
|
||||
temporal_args+=(--class-id "${TEMPORAL_CLASS_ID}")
|
||||
fi
|
||||
|
||||
if [[ -n "${TEMPORAL_FRAME_ID_START}" ]]; then
|
||||
temporal_args+=(--frame-id-start "${TEMPORAL_FRAME_ID_START}")
|
||||
fi
|
||||
|
||||
if [[ -n "${TEMPORAL_FRAME_ID_END}" ]]; then
|
||||
temporal_args+=(--frame-id-end "${TEMPORAL_FRAME_ID_END}")
|
||||
fi
|
||||
|
||||
if [[ -n "${TEMPORAL_TRACK_IDS}" ]]; then
|
||||
# shellcheck disable=SC2206
|
||||
local temporal_track_id_arr=(${TEMPORAL_TRACK_IDS})
|
||||
for track_id in "${temporal_track_id_arr[@]}"; do
|
||||
temporal_args+=(--track-id "${track_id}")
|
||||
done
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "######################################################################"
|
||||
echo "# Feishu issue-data temporal observation"
|
||||
echo "######################################################################"
|
||||
echo "Target path : ${target_path}"
|
||||
if [[ -n "${TEMPORAL_CLASS_ID}" ]]; then
|
||||
echo "Class ID : ${TEMPORAL_CLASS_ID}"
|
||||
fi
|
||||
if [[ -n "${TEMPORAL_TRACK_IDS}" ]]; then
|
||||
echo "Track IDs : ${TEMPORAL_TRACK_IDS}"
|
||||
fi
|
||||
if [[ -n "${TEMPORAL_FRAME_ID_START}" || -n "${TEMPORAL_FRAME_ID_END}" ]]; then
|
||||
echo "Frame ID Range : [${TEMPORAL_FRAME_ID_START:-"-inf"}, ${TEMPORAL_FRAME_ID_END:-"+inf"}]"
|
||||
fi
|
||||
echo "Heading Source : ${TEMPORAL_HEADING_SOURCE}"
|
||||
|
||||
PYTHON_BIN="${PYTHON_BIN}" \
|
||||
RESULTS_ROOT="${RESULTS_ROOT}" \
|
||||
OUTPUT_ROOT="${TEMPORAL_OUTPUT_ROOT}" \
|
||||
OUTPUT_DIR_NAME="${TEMPORAL_OUTPUT_DIR_NAME}" \
|
||||
TRACKING_JSON_NAME="${TEMPORAL_JSON_NAME}" \
|
||||
TEMPORAL_MIN_LENGTH="${TEMPORAL_MIN_LENGTH}" \
|
||||
TEMPORAL_CLASS_ID="${TEMPORAL_CLASS_ID}" \
|
||||
TEMPORAL_TRACK_IDS="${TEMPORAL_TRACK_IDS}" \
|
||||
TEMPORAL_FRAME_ID_START="${TEMPORAL_FRAME_ID_START}" \
|
||||
TEMPORAL_FRAME_ID_END="${TEMPORAL_FRAME_ID_END}" \
|
||||
TEMPORAL_PLOTS="${TEMPORAL_PLOTS}" \
|
||||
TEMPORAL_EXPORT_SERIES="${TEMPORAL_EXPORT_SERIES}" \
|
||||
TEMPORAL_FOCUS_TRACK_PLOTS="${TEMPORAL_FOCUS_TRACK_PLOTS}" \
|
||||
TEMPORAL_PREFER_EGO="${TEMPORAL_PREFER_EGO}" \
|
||||
TEMPORAL_X_AXIS="${TEMPORAL_X_AXIS}" \
|
||||
TEMPORAL_HEADING_SOURCE="${TEMPORAL_HEADING_SOURCE}" \
|
||||
bash "${ISSUE_TEMPORAL_SCRIPT}" "${temporal_args[@]}"
|
||||
}
|
||||
|
||||
if [[ "${#ISSUE_IDS[@]}" -eq 0 ]]; then
|
||||
run_tracking_wrapper "${TARGET_PATH}"
|
||||
workflow_failed=0
|
||||
if [[ "${ENABLE_VISUALIZE}" == "1" ]]; then
|
||||
if ! run_visualization_wrapper "${TARGET_PATH}"; then
|
||||
workflow_failed=1
|
||||
fi
|
||||
fi
|
||||
if [[ "${ENABLE_TEMPORAL_ANALYSIS}" == "1" ]]; then
|
||||
if ! run_temporal_wrapper "${TARGET_PATH}"; then
|
||||
workflow_failed=1
|
||||
fi
|
||||
fi
|
||||
if [[ "${ENABLE_CONVERT}" == "1" ]]; then
|
||||
if ! run_conversion_wrapper "${TARGET_PATH}"; then
|
||||
workflow_failed=1
|
||||
fi
|
||||
fi
|
||||
[[ "${workflow_failed}" -eq 0 ]]
|
||||
exit 0
|
||||
fi
|
||||
|
||||
total_issues=0
|
||||
success_issues=0
|
||||
failed_issues=0
|
||||
|
||||
for issue_index in "${!ISSUE_IDS[@]}"; do
|
||||
issue_id="${ISSUE_IDS[issue_index]}"
|
||||
issue_root="${TARGET_PATH}/issue_${issue_id}"
|
||||
((total_issues += 1))
|
||||
|
||||
echo ""
|
||||
printf '[TRACKING][%d/%d] issue_%s\n' \
|
||||
"$((issue_index + 1))" "${#ISSUE_IDS[@]}" "${issue_id}"
|
||||
|
||||
if [[ ! -d "${issue_root}" ]]; then
|
||||
echo "[FAIL] issue_${issue_id}: not found under ${TARGET_PATH}" >&2
|
||||
((failed_issues += 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
if run_tracking_wrapper "${issue_root}"; then
|
||||
((success_issues += 1))
|
||||
else
|
||||
((failed_issues += 1))
|
||||
echo "[FAIL] issue_${issue_id}: tracking failed" >&2
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
printf '[DONE] issues=%d success=%d failed=%d\n' \
|
||||
"${total_issues}" "${success_issues}" "${failed_issues}"
|
||||
|
||||
workflow_failed=0
|
||||
if [[ "${ENABLE_VISUALIZE}" == "1" ]]; then
|
||||
if [[ "${failed_issues}" -ne 0 ]]; then
|
||||
echo "Skipping visualization because some tracking jobs failed" >&2
|
||||
else
|
||||
if ! run_visualization_wrapper "${TARGET_PATH}"; then
|
||||
workflow_failed=1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "${ENABLE_TEMPORAL_ANALYSIS}" == "1" ]]; then
|
||||
if [[ "${failed_issues}" -ne 0 ]]; then
|
||||
echo "Skipping temporal observation because some tracking jobs failed" >&2
|
||||
else
|
||||
if ! run_temporal_wrapper "${TARGET_PATH}"; then
|
||||
workflow_failed=1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "${ENABLE_CONVERT}" == "1" ]]; then
|
||||
if [[ "${failed_issues}" -ne 0 ]]; then
|
||||
echo "Skipping protocol conversion because some tracking jobs failed" >&2
|
||||
else
|
||||
if ! run_conversion_wrapper "${TARGET_PATH}"; then
|
||||
workflow_failed=1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
[[ "${failed_issues}" -eq 0 && "${workflow_failed}" -eq 0 ]]
|
||||
260
tools/feishu_project/run_issue_data_visualize_tracking.sh
Executable file
260
tools/feishu_project/run_issue_data_visualize_tracking.sh
Executable file
@@ -0,0 +1,260 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
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/feishu_project/inference_issue_data}
|
||||
DOWNLOAD_ROOT=${DOWNLOAD_ROOT:-/data1/dongying/Mono3d/G1Q3/feishu_project/downloaded_issue_data}
|
||||
OUTPUT_ROOT=${OUTPUT_ROOT:-}
|
||||
OUTPUT_DIR_NAME=${OUTPUT_DIR_NAME:-tracking_vis_raw}
|
||||
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=""
|
||||
ISSUE_IDS=()
|
||||
|
||||
while (($# > 0)); do
|
||||
case "$1" in
|
||||
--issue-id)
|
||||
if (($# < 2)); then
|
||||
echo "Error: --issue-id requires a value" >&2
|
||||
exit 1
|
||||
fi
|
||||
ISSUE_IDS+=("$2")
|
||||
shift 2
|
||||
;;
|
||||
--issue-id=*)
|
||||
ISSUE_IDS+=("${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
|
||||
|
||||
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}")"
|
||||
DOWNLOAD_ROOT_ABS="$(resolve_abs_path "${DOWNLOAD_ROOT}")"
|
||||
|
||||
is_case_dir() {
|
||||
local dir_path="$1"
|
||||
[[ -d "${dir_path}" ]] && [[ -f "${dir_path}/${TRACKING_JSON_NAME}" ]]
|
||||
}
|
||||
|
||||
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' "${DOWNLOAD_ROOT_ABS}" "${rel_case_dir}"
|
||||
}
|
||||
|
||||
run_single_case() {
|
||||
local case_dir="$1"
|
||||
local tracking_json="${case_dir}/${TRACKING_JSON_NAME}"
|
||||
local raw_video_path
|
||||
local output_dir
|
||||
local cmd
|
||||
|
||||
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}")"
|
||||
|
||||
cmd=(
|
||||
"${PYTHON_BIN}" "${VISUALIZE_SCRIPT}"
|
||||
--tracking "${tracking_json}"
|
||||
--images "${raw_video_path}"
|
||||
--output "${output_dir}"
|
||||
--align-by-frame-id
|
||||
)
|
||||
|
||||
if [[ -n "${VIS_MAX_FRAMES}" ]]; then
|
||||
cmd+=(--max-frames "${VIS_MAX_FRAMES}")
|
||||
fi
|
||||
|
||||
if [[ -n "${VIS_CLASS_ID}" ]]; then
|
||||
cmd+=(--class-id "${VIS_CLASS_ID}")
|
||||
fi
|
||||
|
||||
if [[ -n "${VIS_TRACK_IDS}" ]]; then
|
||||
# shellcheck disable=SC2206
|
||||
local track_id_arr=(${VIS_TRACK_IDS})
|
||||
cmd+=(--track-ids "${track_id_arr[@]}")
|
||||
fi
|
||||
|
||||
if [[ "${VIS_SHOW_TRAJECTORY}" == "1" ]]; then
|
||||
cmd+=(--show-trajectory)
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "######################################################################"
|
||||
echo "# Feishu issue-data raw-image visualization"
|
||||
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 [[ "${#ISSUE_IDS[@]}" -eq 0 ]]; then
|
||||
if is_case_dir "${TARGET_PATH}"; then
|
||||
run_single_case "${TARGET_PATH}"
|
||||
elif [[ -d "${TARGET_PATH}" ]]; then
|
||||
run_batch_root "${TARGET_PATH}"
|
||||
else
|
||||
echo "Error: unsupported target path: ${TARGET_PATH}" >&2
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
total_issues=0
|
||||
success_issues=0
|
||||
failed_issues=0
|
||||
|
||||
for issue_id in "${ISSUE_IDS[@]}"; do
|
||||
issue_root="${TARGET_PATH}/issue_${issue_id}"
|
||||
((total_issues += 1))
|
||||
|
||||
if [[ ! -d "${issue_root}" ]]; then
|
||||
echo "[FAIL] issue_${issue_id}: not found under ${TARGET_PATH}" >&2
|
||||
((failed_issues += 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
if run_batch_root "${issue_root}"; then
|
||||
((success_issues += 1))
|
||||
else
|
||||
((failed_issues += 1))
|
||||
echo "[FAIL] issue_${issue_id}: visualization failed" >&2
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
printf '[DONE] issues=%d success=%d failed=%d\n' \
|
||||
"${total_issues}" "${success_issues}" "${failed_issues}"
|
||||
|
||||
[[ "${failed_issues}" -eq 0 ]]
|
||||
23
tools/feishu_project/run_issue_tag_profile.sh
Executable file
23
tools/feishu_project/run_issue_tag_profile.sh
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
PROJECT_ROOT=$(cd "${SCRIPT_DIR}/../.." && pwd)
|
||||
|
||||
SYNC_ROOT=${SYNC_ROOT:-/data1/dongying/Mono3d/G1Q3/feishu_project}
|
||||
INPUT_JSON=${INPUT_JSON:-"${SYNC_ROOT}/exports/dongying_g1q3_issue_list.json"}
|
||||
OUTPUT_DIR=${OUTPUT_DIR:-"${SYNC_ROOT}/reports/issue_tag_profile"}
|
||||
REPORT_NAME=${REPORT_NAME:-dongying_g1q3_issue_tag_profile}
|
||||
PYTHON_BIN=${PYTHON_BIN:-/deeplearning_team/ydong/dongying/miniconda/envs/dev/bin/python}
|
||||
TOP_LIMIT=${TOP_LIMIT:-15}
|
||||
|
||||
CMD=(
|
||||
"${PYTHON_BIN}" "${PROJECT_ROOT}/tools/feishu_project/analyze_issue_tag_profile.py"
|
||||
--input-json "${INPUT_JSON}"
|
||||
--output-dir "${OUTPUT_DIR}"
|
||||
--report-name "${REPORT_NAME}"
|
||||
--top-limit "${TOP_LIMIT}"
|
||||
)
|
||||
|
||||
CMD+=("$@")
|
||||
"${CMD[@]}"
|
||||
583
tools/feishu_project/run_roi1_crop_compensation_experiment.py
Executable file
583
tools/feishu_project/run_roi1_crop_compensation_experiment.py
Executable file
@@ -0,0 +1,583 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run ROI1 crop-center compensation experiments and compare detection stability."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import statistics
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
DEFAULT_MODEL = ROOT / "runs" / "export" / "train_mono3d_two_roi_20260416-raw_no_edge" / "merged_model.torchscript"
|
||||
DEFAULT_OUTPUT_ROOT = Path("/data1/dongying/Mono3d/G1Q3/feishu_project/roi1_crop_compensation_experiments")
|
||||
FRAME_FILE_RE = re.compile(r"camera4_(\d+)_")
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--baseline-case-dir", required=True, type=Path, help="Baseline inference case output directory.")
|
||||
parser.add_argument("--video-case-dir", required=True, type=Path, help="Input video case path passed to --video-case-dir.")
|
||||
parser.add_argument("--tracking-json", type=Path, default=None, help="Tracking JSON used to derive the reference target trajectory.")
|
||||
parser.add_argument("--track-id", type=int, default=7)
|
||||
parser.add_argument("--frame-id-start", type=int, required=True)
|
||||
parser.add_argument("--frame-id-end", type=int, required=True)
|
||||
parser.add_argument("--alpha", type=float, default=1.0, help="Scale factor applied to the bbox-derived ROI1 compensation.")
|
||||
parser.add_argument("--exported-model", type=Path, default=DEFAULT_MODEL)
|
||||
parser.add_argument("--device", type=str, default="cuda")
|
||||
parser.add_argument("--output-root", type=Path, default=DEFAULT_OUTPUT_ROOT)
|
||||
parser.add_argument("--skip-existing", action="store_true", help="Reuse existing experiment outputs when present.")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def load_json(path: Path) -> Any:
|
||||
with path.open("r", encoding="utf-8") as file:
|
||||
return json.load(file)
|
||||
|
||||
|
||||
def save_json(path: Path, payload: Any) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("w", encoding="utf-8") as file:
|
||||
json.dump(payload, file, ensure_ascii=False, indent=2)
|
||||
file.write("\n")
|
||||
|
||||
|
||||
def coerce_bbox(values: Any) -> tuple[float, float, float, float] | None:
|
||||
if not isinstance(values, (list, tuple)) or len(values) < 4:
|
||||
return None
|
||||
try:
|
||||
x1, y1, x2, y2 = (float(values[0]), float(values[1]), float(values[2]), float(values[3]))
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
return x1, y1, x2, y2
|
||||
|
||||
|
||||
def bbox_iou(box_a: tuple[float, float, float, float], box_b: tuple[float, float, float, float]) -> float:
|
||||
ax1, ay1, ax2, ay2 = box_a
|
||||
bx1, by1, bx2, by2 = box_b
|
||||
inter_x1 = max(ax1, bx1)
|
||||
inter_y1 = max(ay1, by1)
|
||||
inter_x2 = min(ax2, bx2)
|
||||
inter_y2 = min(ay2, by2)
|
||||
inter_w = max(0.0, inter_x2 - inter_x1)
|
||||
inter_h = max(0.0, inter_y2 - inter_y1)
|
||||
inter_area = inter_w * inter_h
|
||||
if inter_area <= 0.0:
|
||||
return 0.0
|
||||
area_a = max(0.0, ax2 - ax1) * max(0.0, ay2 - ay1)
|
||||
area_b = max(0.0, bx2 - bx1) * max(0.0, by2 - by1)
|
||||
denom = area_a + area_b - inter_area
|
||||
return inter_area / denom if denom > 0.0 else 0.0
|
||||
|
||||
|
||||
def center_distance(box_a: tuple[float, float, float, float], box_b: tuple[float, float, float, float]) -> float:
|
||||
acx = (box_a[0] + box_a[2]) * 0.5
|
||||
acy = (box_a[1] + box_a[3]) * 0.5
|
||||
bcx = (box_b[0] + box_b[2]) * 0.5
|
||||
bcy = (box_b[1] + box_b[3]) * 0.5
|
||||
return math.hypot(acx - bcx, acy - bcy)
|
||||
|
||||
|
||||
def frame_id_from_tracking_frame(frame_data: dict[str, Any], fallback_idx: int) -> int:
|
||||
for det in frame_data.get("detections", []):
|
||||
for key in ("frameId", "frame_id"):
|
||||
value = det.get(key)
|
||||
if value is None:
|
||||
continue
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
frame_info = frame_data.get("frame_info")
|
||||
if isinstance(frame_info, dict):
|
||||
for key in ("frame_id", "frameId", "original_frame_id"):
|
||||
value = frame_info.get(key)
|
||||
if value is None:
|
||||
continue
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
return fallback_idx
|
||||
|
||||
|
||||
def load_reference_track(tracking_json: Path, track_id: int, frame_id_start: int, frame_id_end: int) -> list[dict[str, Any]]:
|
||||
payload = load_json(tracking_json)
|
||||
frames = payload.get("frames", payload) if isinstance(payload, dict) else payload
|
||||
if not isinstance(frames, list):
|
||||
raise ValueError(f"Unsupported tracking JSON structure in {tracking_json}")
|
||||
|
||||
reference = []
|
||||
for frame_idx, frame_data in enumerate(frames):
|
||||
if not isinstance(frame_data, dict):
|
||||
continue
|
||||
frame_id = frame_id_from_tracking_frame(frame_data, frame_idx)
|
||||
if frame_id < frame_id_start or frame_id > frame_id_end:
|
||||
continue
|
||||
for det in frame_data.get("detections", []):
|
||||
if det.get("track_id") != track_id:
|
||||
continue
|
||||
bbox = coerce_bbox(det.get("bbox"))
|
||||
if bbox is None:
|
||||
continue
|
||||
class_id = int(det.get("class_id")) if det.get("class_id") is not None else None
|
||||
reference.append(
|
||||
{
|
||||
"frame_id": frame_id,
|
||||
"frame_idx": frame_idx,
|
||||
"bbox": bbox,
|
||||
"class_id": class_id,
|
||||
"type_name": det.get("type_name"),
|
||||
"y2": bbox[3],
|
||||
}
|
||||
)
|
||||
break
|
||||
|
||||
reference.sort(key=lambda item: item["frame_id"])
|
||||
if not reference:
|
||||
raise FileNotFoundError(
|
||||
f"Track {track_id} not found in {tracking_json} within frame_id [{frame_id_start}, {frame_id_end}]"
|
||||
)
|
||||
return reference
|
||||
|
||||
|
||||
def build_offset_maps(
|
||||
reference_track: list[dict[str, Any]],
|
||||
alpha: float,
|
||||
) -> tuple[dict[int, float], dict[int, float], dict[int, float]]:
|
||||
ref_y2 = float(reference_track[0]["y2"])
|
||||
oracle: dict[int, float] = {}
|
||||
causal: dict[int, float] = {}
|
||||
frame_delta: dict[int, float] = {}
|
||||
|
||||
prev_oracle_offset = 0.0
|
||||
prev_y2 = ref_y2
|
||||
for idx, item in enumerate(reference_track):
|
||||
frame_id = int(item["frame_id"])
|
||||
current_offset = alpha * (float(item["y2"]) - ref_y2)
|
||||
oracle[frame_id] = current_offset
|
||||
causal[frame_id] = prev_oracle_offset if idx > 0 else 0.0
|
||||
frame_delta[frame_id] = alpha * (float(item["y2"]) - prev_y2) if idx > 0 else 0.0
|
||||
prev_oracle_offset = current_offset
|
||||
prev_y2 = float(item["y2"])
|
||||
return oracle, causal, frame_delta
|
||||
|
||||
|
||||
def write_offset_map(path: Path, offsets: dict[int, float], metadata: dict[str, Any]) -> None:
|
||||
save_json(
|
||||
path,
|
||||
{
|
||||
"default_offset_px": 0.0,
|
||||
"frame_id_offsets": {str(frame_id): offset for frame_id, offset in offsets.items()},
|
||||
"metadata": metadata,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def build_prediction_frame_map(predictions_merge_dir: Path) -> dict[int, Path]:
|
||||
frame_map: dict[int, Path] = {}
|
||||
for json_path in sorted(predictions_merge_dir.glob("*.json")):
|
||||
match = FRAME_FILE_RE.search(json_path.name)
|
||||
if not match:
|
||||
continue
|
||||
frame_map[int(match.group(1))] = json_path
|
||||
return frame_map
|
||||
|
||||
|
||||
def extract_candidate_records(frame_json_path: Path) -> list[dict[str, Any]]:
|
||||
payload = load_json(frame_json_path)
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError(f"Unexpected frame prediction structure in {frame_json_path}")
|
||||
|
||||
candidates = []
|
||||
for value in payload.values():
|
||||
if not isinstance(value, dict):
|
||||
continue
|
||||
bbox = coerce_bbox(value.get("box2d"))
|
||||
if bbox is None:
|
||||
continue
|
||||
class_id = value.get("type")
|
||||
try:
|
||||
class_id = int(class_id) if class_id is not None else None
|
||||
except (TypeError, ValueError):
|
||||
class_id = None
|
||||
x_ego = None
|
||||
if isinstance(value.get("box_center_xyz_ego"), list) and value["box_center_xyz_ego"]:
|
||||
try:
|
||||
x_ego = float(value["box_center_xyz_ego"][0])
|
||||
except (TypeError, ValueError):
|
||||
x_ego = None
|
||||
if x_ego is None and isinstance(value.get("xyzlhwyaw_ego"), list) and value["xyzlhwyaw_ego"]:
|
||||
try:
|
||||
x_ego = float(value["xyzlhwyaw_ego"][0])
|
||||
except (TypeError, ValueError):
|
||||
x_ego = None
|
||||
if x_ego is None:
|
||||
continue
|
||||
candidates.append(
|
||||
{
|
||||
"bbox": bbox,
|
||||
"class_id": class_id,
|
||||
"type_name": value.get("type_name"),
|
||||
"score": float(value.get("score", 0.0)),
|
||||
"x_ego": x_ego,
|
||||
}
|
||||
)
|
||||
return candidates
|
||||
|
||||
|
||||
def match_reference_detection(
|
||||
frame_json_path: Path,
|
||||
reference_bbox: tuple[float, float, float, float],
|
||||
reference_class_id: int | None,
|
||||
) -> dict[str, Any] | None:
|
||||
candidates = extract_candidate_records(frame_json_path)
|
||||
same_class = [
|
||||
candidate for candidate in candidates if reference_class_id is not None and candidate["class_id"] == reference_class_id
|
||||
]
|
||||
candidate_pool = same_class or candidates
|
||||
if not candidate_pool:
|
||||
return None
|
||||
|
||||
best = max(
|
||||
candidate_pool,
|
||||
key=lambda candidate: (
|
||||
bbox_iou(candidate["bbox"], reference_bbox),
|
||||
-center_distance(candidate["bbox"], reference_bbox),
|
||||
candidate["score"],
|
||||
),
|
||||
)
|
||||
best = dict(best)
|
||||
best["iou_to_reference"] = bbox_iou(best["bbox"], reference_bbox)
|
||||
return best
|
||||
|
||||
|
||||
def collect_variant_series(reference_track: list[dict[str, Any]], predictions_merge_dir: Path) -> list[dict[str, Any]]:
|
||||
frame_map = build_prediction_frame_map(predictions_merge_dir)
|
||||
series: list[dict[str, Any]] = []
|
||||
for item in reference_track:
|
||||
frame_id = int(item["frame_id"])
|
||||
frame_json_path = frame_map.get(frame_id)
|
||||
if frame_json_path is None:
|
||||
continue
|
||||
matched = match_reference_detection(frame_json_path, item["bbox"], item["class_id"])
|
||||
if matched is None:
|
||||
continue
|
||||
bbox = matched["bbox"]
|
||||
series.append(
|
||||
{
|
||||
"frame_id": frame_id,
|
||||
"x_ego": float(matched["x_ego"]),
|
||||
"score": float(matched["score"]),
|
||||
"iou_to_reference": float(matched["iou_to_reference"]),
|
||||
"bbox": bbox,
|
||||
"y2": bbox[3],
|
||||
"cy": (bbox[1] + bbox[3]) * 0.5,
|
||||
"w": bbox[2] - bbox[0],
|
||||
"h": bbox[3] - bbox[1],
|
||||
}
|
||||
)
|
||||
series.sort(key=lambda item: item["frame_id"])
|
||||
return series
|
||||
|
||||
|
||||
def percentile(sorted_values: list[float], q: float) -> float:
|
||||
if not sorted_values:
|
||||
raise ValueError("sorted_values must not be empty")
|
||||
if q <= 0:
|
||||
return sorted_values[0]
|
||||
if q >= 1:
|
||||
return sorted_values[-1]
|
||||
index = max(0, math.ceil(q * len(sorted_values)) - 1)
|
||||
return sorted_values[index]
|
||||
|
||||
|
||||
def compute_series_metrics(series: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
if len(series) < 2:
|
||||
raise ValueError("Series must contain at least two samples")
|
||||
|
||||
x_values = [item["x_ego"] for item in series]
|
||||
y2_values = [item["y2"] for item in series]
|
||||
cy_values = [item["cy"] for item in series]
|
||||
ious = [item["iou_to_reference"] for item in series]
|
||||
scores = [item["score"] for item in series]
|
||||
|
||||
dx = [x_values[idx] - x_values[idx - 1] for idx in range(1, len(x_values))]
|
||||
dy2 = [y2_values[idx] - y2_values[idx - 1] for idx in range(1, len(y2_values))]
|
||||
dcy = [cy_values[idx] - cy_values[idx - 1] for idx in range(1, len(cy_values))]
|
||||
abs_dx = sorted(abs(value) for value in dx)
|
||||
abs_dy2 = sorted(abs(value) for value in dy2)
|
||||
abs_dcy = sorted(abs(value) for value in dcy)
|
||||
|
||||
window = 5
|
||||
local_dev = []
|
||||
for idx in range(window, len(x_values) - window):
|
||||
local_mean = sum(x_values[idx - window : idx + window + 1]) / (2 * window + 1)
|
||||
local_dev.append(abs(x_values[idx] - local_mean))
|
||||
local_dev_sorted = sorted(local_dev) if local_dev else [0.0]
|
||||
|
||||
return {
|
||||
"samples": len(series),
|
||||
"frame_id_start": int(series[0]["frame_id"]),
|
||||
"frame_id_end": int(series[-1]["frame_id"]),
|
||||
"x_start": float(x_values[0]),
|
||||
"x_end": float(x_values[-1]),
|
||||
"x_change": float(x_values[-1] - x_values[0]),
|
||||
"abs_dx_mean": float(statistics.mean(abs(value) for value in dx)),
|
||||
"abs_dx_p95": float(percentile(abs_dx, 0.95)),
|
||||
"abs_dx_max": float(max(abs_dx)),
|
||||
"abs_dy2_mean": float(statistics.mean(abs(value) for value in dy2)),
|
||||
"abs_dy2_p95": float(percentile(abs_dy2, 0.95)),
|
||||
"abs_dcy_mean": float(statistics.mean(abs(value) for value in dcy)),
|
||||
"abs_dcy_p95": float(percentile(abs_dcy, 0.95)),
|
||||
"local_dev_mean": float(statistics.mean(local_dev_sorted)),
|
||||
"local_dev_p95": float(percentile(local_dev_sorted, 0.95)),
|
||||
"local_dev_max": float(max(local_dev_sorted)),
|
||||
"mean_iou_to_reference": float(statistics.mean(ious)),
|
||||
"min_iou_to_reference": float(min(ious)),
|
||||
"mean_score": float(statistics.mean(scores)),
|
||||
}
|
||||
|
||||
|
||||
def run_inference_variant(
|
||||
*,
|
||||
video_case_dir: Path,
|
||||
output_dir: Path,
|
||||
frame_id_start: int,
|
||||
frame_id_end: int,
|
||||
exported_model: Path,
|
||||
device: str,
|
||||
offset_map_path: Path | None = None,
|
||||
) -> None:
|
||||
cmd = [
|
||||
sys.executable,
|
||||
str(ROOT / "tools" / "model_inference" / "core" / "run_two_roi_exported_onnx_infer.py"),
|
||||
"--video-case-dir",
|
||||
str(video_case_dir),
|
||||
"--output-dir",
|
||||
str(output_dir),
|
||||
"--frame-id-start",
|
||||
str(frame_id_start),
|
||||
"--frame-id-end",
|
||||
str(frame_id_end),
|
||||
"--video-stride",
|
||||
"1",
|
||||
"--exported-model",
|
||||
str(exported_model),
|
||||
"--device",
|
||||
device,
|
||||
"--enable-cross-class-merge-prior",
|
||||
"--save-aggregate-predictions",
|
||||
]
|
||||
if offset_map_path is not None:
|
||||
cmd.extend(["--roi1-crop-center-y-offset-map", str(offset_map_path)])
|
||||
subprocess.run(cmd, check=True, cwd=str(ROOT))
|
||||
|
||||
|
||||
def build_report_payload(
|
||||
*,
|
||||
args: argparse.Namespace,
|
||||
reference_track: list[dict[str, Any]],
|
||||
baseline_metrics: dict[str, Any],
|
||||
oracle_metrics: dict[str, Any],
|
||||
causal_metrics: dict[str, Any],
|
||||
frame_delta_metrics: dict[str, Any],
|
||||
oracle_offsets_path: Path,
|
||||
causal_offsets_path: Path,
|
||||
frame_delta_offsets_path: Path,
|
||||
baseline_dir: Path,
|
||||
oracle_dir: Path,
|
||||
causal_dir: Path,
|
||||
frame_delta_dir: Path,
|
||||
) -> dict[str, Any]:
|
||||
baseline_abs_dx_p95 = baseline_metrics["abs_dx_p95"]
|
||||
baseline_local_dev_p95 = baseline_metrics["local_dev_p95"]
|
||||
|
||||
def compare_metrics(metrics: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"abs_dx_p95_delta": float(metrics["abs_dx_p95"] - baseline_abs_dx_p95),
|
||||
"abs_dx_p95_reduction_ratio": float((baseline_abs_dx_p95 - metrics["abs_dx_p95"]) / baseline_abs_dx_p95)
|
||||
if baseline_abs_dx_p95 > 0
|
||||
else 0.0,
|
||||
"local_dev_p95_delta": float(metrics["local_dev_p95"] - baseline_local_dev_p95),
|
||||
"local_dev_p95_reduction_ratio": float((baseline_local_dev_p95 - metrics["local_dev_p95"]) / baseline_local_dev_p95)
|
||||
if baseline_local_dev_p95 > 0
|
||||
else 0.0,
|
||||
}
|
||||
|
||||
return {
|
||||
"track_id": int(args.track_id),
|
||||
"frame_id_start": int(args.frame_id_start),
|
||||
"frame_id_end": int(args.frame_id_end),
|
||||
"alpha": float(args.alpha),
|
||||
"exported_model": str(args.exported_model.resolve()),
|
||||
"baseline_case_dir": str(baseline_dir.resolve()),
|
||||
"video_case_dir": str(args.video_case_dir.resolve()),
|
||||
"reference_track_frames": len(reference_track),
|
||||
"oracle_offset_map": str(oracle_offsets_path.resolve()),
|
||||
"causal_offset_map": str(causal_offsets_path.resolve()),
|
||||
"frame_delta_offset_map": str(frame_delta_offsets_path.resolve()),
|
||||
"variants": {
|
||||
"baseline": {
|
||||
"output_dir": str(baseline_dir.resolve()),
|
||||
"metrics": baseline_metrics,
|
||||
},
|
||||
"oracle": {
|
||||
"output_dir": str(oracle_dir.resolve()),
|
||||
"metrics": oracle_metrics,
|
||||
"comparison_to_baseline": compare_metrics(oracle_metrics),
|
||||
},
|
||||
"causal": {
|
||||
"output_dir": str(causal_dir.resolve()),
|
||||
"metrics": causal_metrics,
|
||||
"comparison_to_baseline": compare_metrics(causal_metrics),
|
||||
},
|
||||
"frame_delta": {
|
||||
"output_dir": str(frame_delta_dir.resolve()),
|
||||
"metrics": frame_delta_metrics,
|
||||
"comparison_to_baseline": compare_metrics(frame_delta_metrics),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
baseline_case_dir = args.baseline_case_dir.resolve()
|
||||
tracking_json = args.tracking_json.resolve() if args.tracking_json else baseline_case_dir / "merge.json"
|
||||
|
||||
reference_track = load_reference_track(
|
||||
tracking_json=tracking_json,
|
||||
track_id=args.track_id,
|
||||
frame_id_start=args.frame_id_start,
|
||||
frame_id_end=args.frame_id_end,
|
||||
)
|
||||
oracle_offsets, causal_offsets, frame_delta_offsets = build_offset_maps(reference_track, alpha=args.alpha)
|
||||
|
||||
case_tag = f"{baseline_case_dir.name}_track{args.track_id}_f{args.frame_id_start}_{args.frame_id_end}_a{str(args.alpha).replace('.', 'p')}"
|
||||
output_root = args.output_root.resolve() / case_tag
|
||||
configs_dir = output_root / "configs"
|
||||
oracle_offsets_path = configs_dir / "oracle_roi1_offsets.json"
|
||||
causal_offsets_path = configs_dir / "causal_prev_frame_roi1_offsets.json"
|
||||
frame_delta_offsets_path = configs_dir / "frame_delta_roi1_offsets.json"
|
||||
oracle_dir = output_root / "oracle"
|
||||
causal_dir = output_root / "causal"
|
||||
frame_delta_dir = output_root / "frame_delta"
|
||||
report_path = output_root / "experiment_summary.json"
|
||||
|
||||
write_offset_map(
|
||||
oracle_offsets_path,
|
||||
oracle_offsets,
|
||||
{
|
||||
"mode": "same_frame_oracle",
|
||||
"track_id": args.track_id,
|
||||
"alpha": args.alpha,
|
||||
"reference_frame_id": reference_track[0]["frame_id"],
|
||||
"reference_y2": reference_track[0]["y2"],
|
||||
},
|
||||
)
|
||||
write_offset_map(
|
||||
causal_offsets_path,
|
||||
causal_offsets,
|
||||
{
|
||||
"mode": "previous_frame_causal",
|
||||
"track_id": args.track_id,
|
||||
"alpha": args.alpha,
|
||||
"reference_frame_id": reference_track[0]["frame_id"],
|
||||
"reference_y2": reference_track[0]["y2"],
|
||||
},
|
||||
)
|
||||
write_offset_map(
|
||||
frame_delta_offsets_path,
|
||||
frame_delta_offsets,
|
||||
{
|
||||
"mode": "same_frame_prev_delta",
|
||||
"track_id": args.track_id,
|
||||
"alpha": args.alpha,
|
||||
"reference_frame_id": reference_track[0]["frame_id"],
|
||||
"reference_y2": reference_track[0]["y2"],
|
||||
},
|
||||
)
|
||||
|
||||
if not args.skip_existing or not (oracle_dir / "predictions" / "merge").is_dir():
|
||||
run_inference_variant(
|
||||
video_case_dir=args.video_case_dir.resolve(),
|
||||
output_dir=oracle_dir,
|
||||
frame_id_start=args.frame_id_start,
|
||||
frame_id_end=args.frame_id_end,
|
||||
exported_model=args.exported_model.resolve(),
|
||||
device=args.device,
|
||||
offset_map_path=oracle_offsets_path,
|
||||
)
|
||||
|
||||
if not args.skip_existing or not (causal_dir / "predictions" / "merge").is_dir():
|
||||
run_inference_variant(
|
||||
video_case_dir=args.video_case_dir.resolve(),
|
||||
output_dir=causal_dir,
|
||||
frame_id_start=args.frame_id_start,
|
||||
frame_id_end=args.frame_id_end,
|
||||
exported_model=args.exported_model.resolve(),
|
||||
device=args.device,
|
||||
offset_map_path=causal_offsets_path,
|
||||
)
|
||||
|
||||
if not args.skip_existing or not (frame_delta_dir / "predictions" / "merge").is_dir():
|
||||
run_inference_variant(
|
||||
video_case_dir=args.video_case_dir.resolve(),
|
||||
output_dir=frame_delta_dir,
|
||||
frame_id_start=args.frame_id_start,
|
||||
frame_id_end=args.frame_id_end,
|
||||
exported_model=args.exported_model.resolve(),
|
||||
device=args.device,
|
||||
offset_map_path=frame_delta_offsets_path,
|
||||
)
|
||||
|
||||
baseline_series = collect_variant_series(reference_track, baseline_case_dir / "predictions" / "merge")
|
||||
oracle_series = collect_variant_series(reference_track, oracle_dir / "predictions" / "merge")
|
||||
causal_series = collect_variant_series(reference_track, causal_dir / "predictions" / "merge")
|
||||
frame_delta_series = collect_variant_series(reference_track, frame_delta_dir / "predictions" / "merge")
|
||||
|
||||
baseline_metrics = compute_series_metrics(baseline_series)
|
||||
oracle_metrics = compute_series_metrics(oracle_series)
|
||||
causal_metrics = compute_series_metrics(causal_series)
|
||||
frame_delta_metrics = compute_series_metrics(frame_delta_series)
|
||||
|
||||
report_payload = build_report_payload(
|
||||
args=args,
|
||||
reference_track=reference_track,
|
||||
baseline_metrics=baseline_metrics,
|
||||
oracle_metrics=oracle_metrics,
|
||||
causal_metrics=causal_metrics,
|
||||
frame_delta_metrics=frame_delta_metrics,
|
||||
oracle_offsets_path=oracle_offsets_path,
|
||||
causal_offsets_path=causal_offsets_path,
|
||||
frame_delta_offsets_path=frame_delta_offsets_path,
|
||||
baseline_dir=baseline_case_dir,
|
||||
oracle_dir=oracle_dir,
|
||||
causal_dir=causal_dir,
|
||||
frame_delta_dir=frame_delta_dir,
|
||||
)
|
||||
save_json(report_path, report_payload)
|
||||
|
||||
print("")
|
||||
print("ROI1 crop compensation experiment summary")
|
||||
print(f"summary_json: {report_path}")
|
||||
print(f"baseline abs_dx_p95 : {baseline_metrics['abs_dx_p95']:.4f} m/frame")
|
||||
print(f"oracle abs_dx_p95 : {oracle_metrics['abs_dx_p95']:.4f} m/frame")
|
||||
print(f"causal abs_dx_p95 : {causal_metrics['abs_dx_p95']:.4f} m/frame")
|
||||
print(f"delta abs_dx_p95 : {frame_delta_metrics['abs_dx_p95']:.4f} m/frame")
|
||||
print(f"baseline local_dev_p95 : {baseline_metrics['local_dev_p95']:.4f} m")
|
||||
print(f"oracle local_dev_p95 : {oracle_metrics['local_dev_p95']:.4f} m")
|
||||
print(f"causal local_dev_p95 : {causal_metrics['local_dev_p95']:.4f} m")
|
||||
print(f"delta local_dev_p95 : {frame_delta_metrics['local_dev_p95']:.4f} m")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
219
tools/feishu_project/skill/feishu-doc/SKILL.md
Executable file
219
tools/feishu_project/skill/feishu-doc/SKILL.md
Executable file
@@ -0,0 +1,219 @@
|
||||
---
|
||||
name: feishu-doc
|
||||
description: Read, create, append, replace, and structurally edit Feishu Docx documents with the feishu_doc tool. Use when Codex needs to work with Feishu docs, cloud docs, docx links, document blocks, tables, images, or file attachments in Feishu/Lark documents.
|
||||
---
|
||||
|
||||
# Feishu Doc
|
||||
|
||||
Use the `feishu_doc` tool for Feishu Docx operations.
|
||||
|
||||
If the current environment does not expose `feishu_doc`, tell the user that the required tool is unavailable and stop instead of inventing a workaround.
|
||||
|
||||
## Extract the doc token
|
||||
|
||||
Extract `doc_token` from the document URL.
|
||||
|
||||
Example:
|
||||
|
||||
- `https://xxx.feishu.cn/docx/ABC123def` -> `ABC123def`
|
||||
|
||||
## Start with the right workflow
|
||||
|
||||
Choose the lightest workflow that fits the request.
|
||||
|
||||
### Read a document
|
||||
|
||||
1. Call `{"action":"read","doc_token":"..."}` first.
|
||||
2. Inspect `hint`, `block_types`, and summary fields in the response.
|
||||
3. If the document contains structured content such as tables or images, call `{"action":"list_blocks","doc_token":"..."}`.
|
||||
4. Use `get_block` only when a single block needs closer inspection.
|
||||
|
||||
### Replace or append text content
|
||||
|
||||
Use markdown-oriented actions for plain document content:
|
||||
|
||||
- `write`: replace the entire document
|
||||
- `append`: append content to the end
|
||||
|
||||
Use markdown for headings, lists, code blocks, quotes, links, and images.
|
||||
|
||||
Do not rely on markdown tables. Use table actions instead.
|
||||
|
||||
### Perform block-level edits
|
||||
|
||||
Use these actions when the user wants targeted updates instead of full-document replacement:
|
||||
|
||||
- `list_blocks`
|
||||
- `get_block`
|
||||
- `update_block`
|
||||
- `delete_block`
|
||||
|
||||
Prefer block-level edits when preserving surrounding content matters.
|
||||
|
||||
## Core actions
|
||||
|
||||
### Read
|
||||
|
||||
```json
|
||||
{ "action": "read", "doc_token": "ABC123def" }
|
||||
```
|
||||
|
||||
### Write the whole document
|
||||
|
||||
```json
|
||||
{ "action": "write", "doc_token": "ABC123def", "content": "# Title\n\nMarkdown content..." }
|
||||
```
|
||||
|
||||
### Append content
|
||||
|
||||
```json
|
||||
{ "action": "append", "doc_token": "ABC123def", "content": "Additional content" }
|
||||
```
|
||||
|
||||
### Create a document
|
||||
|
||||
Always pass the requesting user's `open_id` as `owner_open_id` when available so the user automatically gets access to the new document.
|
||||
|
||||
```json
|
||||
{ "action": "create", "title": "New Document", "owner_open_id": "ou_xxx" }
|
||||
```
|
||||
|
||||
With a folder:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "create",
|
||||
"title": "New Document",
|
||||
"folder_token": "fldcnXXX",
|
||||
"owner_open_id": "ou_xxx"
|
||||
}
|
||||
```
|
||||
|
||||
### Inspect or edit blocks
|
||||
|
||||
```json
|
||||
{ "action": "list_blocks", "doc_token": "ABC123def" }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "action": "get_block", "doc_token": "ABC123def", "block_id": "doxcnXXX" }
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "update_block",
|
||||
"doc_token": "ABC123def",
|
||||
"block_id": "doxcnXXX",
|
||||
"content": "New text"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{ "action": "delete_block", "doc_token": "ABC123def", "block_id": "doxcnXXX" }
|
||||
```
|
||||
|
||||
## Tables
|
||||
|
||||
Use table actions for real Docx tables.
|
||||
|
||||
### Create an empty table
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "create_table",
|
||||
"doc_token": "ABC123def",
|
||||
"row_size": 2,
|
||||
"column_size": 2,
|
||||
"column_width": [200, 200]
|
||||
}
|
||||
```
|
||||
|
||||
Optional: include `parent_block_id` to insert under a specific block.
|
||||
|
||||
### Fill table cells
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "write_table_cells",
|
||||
"doc_token": "ABC123def",
|
||||
"table_block_id": "doxcnTABLE",
|
||||
"values": [
|
||||
["A1", "B1"],
|
||||
["A2", "B2"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Create and fill a table in one step
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "create_table_with_values",
|
||||
"doc_token": "ABC123def",
|
||||
"row_size": 2,
|
||||
"column_size": 2,
|
||||
"column_width": [200, 200],
|
||||
"values": [
|
||||
["A1", "B1"],
|
||||
["A2", "B2"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Images and file attachments
|
||||
|
||||
### Upload an image
|
||||
|
||||
Use exactly one of `url` or `file_path`.
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "upload_image",
|
||||
"doc_token": "ABC123def",
|
||||
"url": "https://example.com/image.png"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "upload_image",
|
||||
"doc_token": "ABC123def",
|
||||
"file_path": "/tmp/image.png",
|
||||
"parent_block_id": "doxcnParent",
|
||||
"index": 5
|
||||
}
|
||||
```
|
||||
|
||||
For small images, scale the asset before uploading so it displays at a useful size in the document.
|
||||
|
||||
### Upload a file attachment
|
||||
|
||||
Use exactly one of `url` or `file_path`.
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "upload_file",
|
||||
"doc_token": "ABC123def",
|
||||
"url": "https://example.com/report.pdf"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "upload_file",
|
||||
"doc_token": "ABC123def",
|
||||
"file_path": "/tmp/report.pdf",
|
||||
"filename": "Q1-report.pdf"
|
||||
}
|
||||
```
|
||||
|
||||
Optional: include `parent_block_id`.
|
||||
|
||||
## Operating rules
|
||||
|
||||
- Start with `read` for unfamiliar documents.
|
||||
- Escalate to `list_blocks` when `read` indicates structured content.
|
||||
- Use `write` only when replacing the full document is intended.
|
||||
- Prefer block-level edits when the user asks for localized changes.
|
||||
- Use table actions instead of markdown tables.
|
||||
- Preserve the user's access by passing `owner_open_id` on document creation when that identity is available.
|
||||
4
tools/feishu_project/skill/feishu-doc/agents/openai.yaml
Executable file
4
tools/feishu_project/skill/feishu-doc/agents/openai.yaml
Executable file
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Feishu Doc"
|
||||
short_description: "Read and edit Feishu Docx documents with the feishu_doc tool."
|
||||
default_prompt: "Use the feishu_doc tool to read, create, append, and structurally edit Feishu Docx documents when the user asks about Feishu docs, cloud docs, or docx links."
|
||||
214
tools/feishu_project/skill/feishu-project-issue-data/SKILL.md
Executable file
214
tools/feishu_project/skill/feishu-project-issue-data/SKILL.md
Executable file
@@ -0,0 +1,214 @@
|
||||
---
|
||||
name: feishu-project-issue-data
|
||||
description: Use when working in the yolo26-3d repository and the user wants to read Feishu Project issue views, export a view to structured JSON, classify issue data addresses, download issue data, repair affected standard-path downloads, validate case completeness, or run batch inference on downloaded issue data. Covers fp CLI plus tools/feishu_project/export_feishu_view_issues.py, download_issue_data.py/sh, and run_issue_data_inference.py/sh, including the 董颖-G1Q3 workflow.
|
||||
---
|
||||
|
||||
# Feishu Project Issue Data
|
||||
|
||||
Use the repo dev env for Python commands:
|
||||
|
||||
```bash
|
||||
/root/.codex/skills/use-dongying-dev-env/scripts/with-dev-env.sh python ...
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```bash
|
||||
/deeplearning_team/ydong/dongying/miniconda/envs/dev/bin/python ...
|
||||
```
|
||||
|
||||
Current repo defaults are documented in [../../feishu_project.md](../../feishu_project.md).
|
||||
|
||||
## When to use
|
||||
|
||||
- Read or verify a Feishu Project issue view with `fp`
|
||||
- Export a view such as `董颖-G1Q3` to structured JSON
|
||||
- Work with `问题数据地址` / `问题数据地址_PDCL`
|
||||
- Analyze issue tag profiles across `目标` / `问题`
|
||||
- Download issue data into the local workspace
|
||||
- Repair historical bad copies that only copied `sigmastar.1`
|
||||
- Check whether downloaded cases are inference-ready
|
||||
- Batch-run exported-model inference on downloaded issue data
|
||||
|
||||
## Core workflow
|
||||
|
||||
### 1. Verify Feishu access and view contents
|
||||
|
||||
Use `fp` directly when the user wants current data.
|
||||
|
||||
```bash
|
||||
fp view list -p <project_key> -u <user_key> -t issue --name "<view_name>"
|
||||
fp workitem list -o json -p <project_key> -u <user_key> --view "<view_name>" --all
|
||||
fp workitem get <issue_id> -o json -p <project_key> -u <user_key> -t issue
|
||||
```
|
||||
|
||||
### 2. Export structured issue JSON
|
||||
|
||||
Use [../../export_feishu_view_issues.py](../../export_feishu_view_issues.py).
|
||||
|
||||
```bash
|
||||
python ../../export_feishu_view_issues.py \
|
||||
--project-key <project_key> \
|
||||
--user-key <user_key> \
|
||||
--view-name "<view_name>" \
|
||||
--output ../../dongying_g1q3_issue_list.json
|
||||
```
|
||||
|
||||
The export should include:
|
||||
|
||||
- `缺陷标签池`
|
||||
- `问题数据地址`
|
||||
- `问题数据地址_PDCL`
|
||||
- `问题发生frameid`
|
||||
|
||||
### 3. Interpret data-address fields
|
||||
|
||||
Treat these as the same download class:
|
||||
|
||||
- pure `ADAS_...::...` clip references
|
||||
- `mdi raw -r ...` commands
|
||||
|
||||
Use this rule:
|
||||
|
||||
- `ADAS_xxx::yyy` is equivalent to `mdi raw -r ADAS_xxx::yyy -s .`
|
||||
|
||||
Standard paths use these normalization rules:
|
||||
|
||||
- rewrite `hfs/project-G1M3` or `project-G1M3` to `G1M3` when needed
|
||||
- if the path ends with `sigmastar.1`, copy the parent case dir
|
||||
- if the path ends with `sigmastar.1/camera4.bin`, copy the case dir above it
|
||||
- if the copied case has no local `test_data/calibs/camera4.json`, sync a shared parent `test_data` directory when present
|
||||
|
||||
### 4. Analyze issue tag profiles
|
||||
|
||||
Use [../../analyze_issue_tag_profile.py](../../analyze_issue_tag_profile.py) directly, or the G1Q3 wrapper
|
||||
[../../run_issue_tag_profile.sh](../../run_issue_tag_profile.sh).
|
||||
|
||||
```bash
|
||||
bash ../../run_issue_tag_profile.sh
|
||||
```
|
||||
|
||||
Defaults:
|
||||
|
||||
- input JSON: `/data1/dongying/Mono3d/G1Q3/feishu_project/exports/dongying_g1q3_issue_list.json`
|
||||
- report root: `/data1/dongying/Mono3d/G1Q3/feishu_project/reports/issue_tag_profile`
|
||||
|
||||
Output is a single HTML profile report with inline SVG donut charts and `场地问题` / `路测问题` toggle views covering:
|
||||
|
||||
- label completeness and multi-label quality
|
||||
- `目标` / `问题` distributions
|
||||
- `目标 x 问题` matrix
|
||||
- function-domain x problem matrix
|
||||
|
||||
Publish the latest HTML to the static intranet site directory with
|
||||
[../../publish_issue_tag_profile_site.sh](../../publish_issue_tag_profile_site.sh).
|
||||
|
||||
```bash
|
||||
bash ../../publish_issue_tag_profile_site.sh
|
||||
```
|
||||
|
||||
To show or serve through a fixed intranet address, pass `INTRANET_HOST` or
|
||||
`PUBLIC_HOST`:
|
||||
|
||||
```bash
|
||||
INTRANET_HOST=192.168.2.169 bash ../../publish_issue_tag_profile_site.sh
|
||||
INTRANET_HOST=192.168.2.169 bash ../../serve_issue_tag_profile_site.sh restart
|
||||
```
|
||||
|
||||
The address must either belong to the current host or be handled by a reverse
|
||||
proxy/static server at that intranet machine.
|
||||
|
||||
Defaults:
|
||||
|
||||
- site root: `/data1/dongying/Mono3d/G1Q3/feishu_project/site`
|
||||
- site index: `/data1/dongying/Mono3d/G1Q3/feishu_project/site/issue_tag_profile/index.html`
|
||||
- example URL: `http://<intranet-host>:8088/issue_tag_profile/`
|
||||
- Nginx example: [../../nginx_issue_tag_profile.conf.example](../../nginx_issue_tag_profile.conf.example)
|
||||
|
||||
If Nginx is not available yet, start a lightweight static server with
|
||||
[../../serve_issue_tag_profile_site.sh](../../serve_issue_tag_profile_site.sh):
|
||||
|
||||
```bash
|
||||
bash ../../serve_issue_tag_profile_site.sh start
|
||||
```
|
||||
|
||||
### 5. Download or repair issue data
|
||||
|
||||
Use [../../download_issue_data.sh](../../download_issue_data.sh) or [../../download_issue_data.py](../../download_issue_data.py).
|
||||
|
||||
Common modes:
|
||||
|
||||
```bash
|
||||
DRY_RUN=1 bash ../../download_issue_data.sh
|
||||
```
|
||||
|
||||
```bash
|
||||
ONLY_REDOWNLOAD_AFFECTED_CASES=1 bash ../../download_issue_data.sh
|
||||
```
|
||||
|
||||
```bash
|
||||
SKIP_MDI=1 bash ../../download_issue_data.sh --issue-id <id>
|
||||
```
|
||||
|
||||
Defaults:
|
||||
|
||||
- download root: `/data1/dongying/Mono3d/G1Q3/feishu_project/downloaded_issue_data`
|
||||
- manifest: `<download_root>/download_manifest.json`
|
||||
|
||||
Use `ONLY_REDOWNLOAD_AFFECTED_CASES=1` to repair old standard-path copies that previously kept only `sigmastar.1`.
|
||||
|
||||
### 6. Validate inference readiness
|
||||
|
||||
Use the same case-resolution rules as [../../../model_inference/adapters/video_dir_inference_utils.py](../../../model_inference/adapters/video_dir_inference_utils.py).
|
||||
|
||||
A valid case must resolve:
|
||||
|
||||
- `*/sigmastar.1/camera4.bin`
|
||||
- a reachable `camera4.json` from one of:
|
||||
- `case_dir/test_data/calibs/camera4.json`
|
||||
- `case_dir.parent/test_data/calibs/camera4.json`
|
||||
- `case_dir/sigmastar.1/calibs/camera4.json`
|
||||
- `case_dir/calibs/camera4.json`
|
||||
|
||||
Prefer validating with the actual inference path-resolution logic instead of ad hoc file checks.
|
||||
|
||||
### 7. Run batch inference on downloaded issue data
|
||||
|
||||
Use [../../run_issue_data_inference.sh](../../run_issue_data_inference.sh) or [../../run_issue_data_inference.py](../../run_issue_data_inference.py).
|
||||
|
||||
```bash
|
||||
DRY_RUN=1 bash ../../run_issue_data_inference.sh
|
||||
```
|
||||
|
||||
```bash
|
||||
bash ../../run_issue_data_inference.sh
|
||||
```
|
||||
|
||||
Behavior:
|
||||
|
||||
- recursively scans the download root for `*/sigmastar.1/camera4.bin`
|
||||
- calls [../../../model_inference/core/run_two_roi_exported_onnx_infer.py](../../../model_inference/core/run_two_roi_exported_onnx_infer.py) with `--video-case-dir`
|
||||
- mirrors the download-tree relative layout into the inference output root
|
||||
|
||||
Defaults:
|
||||
|
||||
- inference root: `/data1/dongying/Mono3d/G1Q3/feishu_project/inference_issue_data`
|
||||
- manifest: `<inference_root>/inference_manifest.json`
|
||||
|
||||
Useful flags:
|
||||
|
||||
- `SKIP_EXISTING=1`
|
||||
- `ENABLE_ATTR=1`
|
||||
- `SAVE_AGGREGATE_PREDICTIONS=1`
|
||||
- `VIDEO_STRIDE=<n>`
|
||||
- `MAX_IMAGES=<n>`
|
||||
|
||||
## Current repo artifacts
|
||||
|
||||
These files are useful outputs, but they are not the source of truth for latest Feishu data:
|
||||
|
||||
- [../../dongying_g1q3_issue_list.json](../../dongying_g1q3_issue_list.json)
|
||||
- [../../dongying_g1q3_data_address_summary.md](../../dongying_g1q3_data_address_summary.md)
|
||||
- [../../dongying_g1q3_data_address_catalog.md](../../dongying_g1q3_data_address_catalog.md)
|
||||
|
||||
If the user asks for latest status, re-query Feishu with `fp` and regenerate outputs instead of trusting stale local exports.
|
||||
3
tools/feishu_project/start_profile.sh
Executable file
3
tools/feishu_project/start_profile.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
python3 -m http.server 8088 \
|
||||
--bind 0.0.0.0 \
|
||||
--directory /data1/dongying/Mono3d/G1Q3/feishu_project/site
|
||||
164
tools/feishu_project/sync_g1q3_issue_data.sh
Executable file
164
tools/feishu_project/sync_g1q3_issue_data.sh
Executable file
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
PROJECT_ROOT=$(cd "${SCRIPT_DIR}/../.." && pwd)
|
||||
|
||||
PROJECT_KEY=${PROJECT_KEY:-68ef617fb371dc80a10641f7}
|
||||
USER_KEY=${USER_KEY:-7550145433285312514}
|
||||
VIEW_NAME=${VIEW_NAME:-董颖-G1Q3}
|
||||
WORK_ITEM_TYPE=${WORK_ITEM_TYPE:-issue}
|
||||
|
||||
SYNC_ROOT=${SYNC_ROOT:-/data1/dongying/Mono3d/G1Q3/feishu_project}
|
||||
SYNC_MANIFEST_PATH_WAS_SET=${SYNC_MANIFEST_PATH+x}
|
||||
INFERENCE_ROOT_WAS_SET=${INFERENCE_ROOT+x}
|
||||
INFERENCE_MANIFEST_PATH_WAS_SET=${INFERENCE_MANIFEST_PATH+x}
|
||||
EXPORT_JSON=${EXPORT_JSON:-"${SYNC_ROOT}/exports/dongying_g1q3_issue_list.json"}
|
||||
SYNC_MANIFEST_PATH=${SYNC_MANIFEST_PATH:-"${SYNC_ROOT}/exports/dongying_g1q3_issue_list.sync_manifest.json"}
|
||||
SNAPSHOT_DIR=${SNAPSHOT_DIR:-"${SYNC_ROOT}/exports/history"}
|
||||
DOWNLOAD_ROOT=${DOWNLOAD_ROOT:-"${SYNC_ROOT}/downloaded_issue_data"}
|
||||
DOWNLOAD_MANIFEST_PATH=${DOWNLOAD_MANIFEST_PATH:-"${DOWNLOAD_ROOT}/download_manifest.json"}
|
||||
INFERENCE_ROOT=${INFERENCE_ROOT:-"${SYNC_ROOT}/inference_issue_data"}
|
||||
INFERENCE_MANIFEST_PATH=${INFERENCE_MANIFEST_PATH:-"${INFERENCE_ROOT}/inference_manifest.json"}
|
||||
|
||||
PYTHON_BIN=${PYTHON_BIN:-/deeplearning_team/ydong/dongying/miniconda/envs/dev/bin/python}
|
||||
RUN_DOWNLOAD=${RUN_DOWNLOAD:-1}
|
||||
RUN_INFERENCE=${RUN_INFERENCE:-1}
|
||||
ENABLE_TRACKING=${ENABLE_TRACKING:-1}
|
||||
ENABLE_CONVERT=${ENABLE_CONVERT:-1}
|
||||
CONVERT_OUTPUT_DIR_NAME=${CONVERT_OUTPUT_DIR_NAME:-objectlist}
|
||||
CONVERT_TRACKING_JSON_NAME=${CONVERT_TRACKING_JSON_NAME:-merge.json}
|
||||
CONVERT_CAM_ID=${CONVERT_CAM_ID:-}
|
||||
SAVE_SNAPSHOT=${SAVE_SNAPSHOT:-1}
|
||||
REFRESH_CHANGED_ISSUES=${REFRESH_CHANGED_ISSUES:-0}
|
||||
SKIP_EXISTING_INFERENCE=${SKIP_EXISTING_INFERENCE:-1}
|
||||
DRY_RUN=${DRY_RUN:-0}
|
||||
USE_ISSUE_FRAME_WINDOW=${USE_ISSUE_FRAME_WINDOW:-1}
|
||||
FRAME_BEFORE=${FRAME_BEFORE:-200}
|
||||
FRAME_AFTER=${FRAME_AFTER:-200}
|
||||
MISSING_ISSUE_FRAME_POLICY=${MISSING_ISSUE_FRAME_POLICY:-full}
|
||||
ISSUE_ID_MIN=${ISSUE_ID_MIN:-}
|
||||
ISSUE_ID_MAX=${ISSUE_ID_MAX:-6979030662}
|
||||
|
||||
EXPORTED_MODEL=${EXPORTED_MODEL:-${PROJECT_ROOT}/runs/export/train_mono3d_two_roi_20260427-raw_no_edge/merged_model.torchscript}
|
||||
# EXPORTED_MODEL=${EXPORTED_MODEL:-${PROJECT_ROOT}/runs/export/train_mono3d_two_roi_20260506-keep_fake_3d_branch/merged_model.torchscript}
|
||||
|
||||
ISSUE_TRACKING_SCRIPT=${ISSUE_TRACKING_SCRIPT:-"${PROJECT_ROOT}/tools/feishu_project/run_issue_data_tracking.sh"}
|
||||
TRACKING_MODEL_VERSION=${TRACKING_MODEL_VERSION:-20260427}
|
||||
ENABLE_RETEST=${ENABLE_RETEST:-1}
|
||||
RETEST_NAME_KEYWORDS=${RETEST_NAME_KEYWORDS:-AEB,FCW,场地} # AEB,FCW,场地 ACC,HMI,LCC,TSR
|
||||
RETEST_RUN_NAME=${RETEST_RUN_NAME:-$(date +%Y%m%d_%H%M%S)}
|
||||
RETEST_ROOT=${RETEST_ROOT:-"${SYNC_ROOT}/retest_runs/${TRACKING_MODEL_VERSION}_${RETEST_RUN_NAME}"}
|
||||
SHOW_DISTANCE_LABEL=${SHOW_DISTANCE_LABEL:-1}
|
||||
DISTANCE_LABEL_MODE=${DISTANCE_LABEL_MODE:-depth}
|
||||
DISTANCE_LABEL_PANELS=${DISTANCE_LABEL_PANELS:-3d}
|
||||
|
||||
export ENABLE_CONVERT
|
||||
export CONVERT_OUTPUT_DIR_NAME
|
||||
export CONVERT_TRACKING_JSON_NAME
|
||||
export CONVERT_CAM_ID
|
||||
|
||||
if [[ "${ENABLE_RETEST}" == "1" ]]; then
|
||||
if [[ -z "${INFERENCE_ROOT_WAS_SET}" ]]; then
|
||||
INFERENCE_ROOT="${RETEST_ROOT}/inference_issue_data"
|
||||
fi
|
||||
if [[ -z "${INFERENCE_MANIFEST_PATH_WAS_SET}" ]]; then
|
||||
INFERENCE_MANIFEST_PATH="${INFERENCE_ROOT}/inference_manifest.json"
|
||||
fi
|
||||
if [[ -z "${SYNC_MANIFEST_PATH_WAS_SET}" ]]; then
|
||||
SYNC_MANIFEST_PATH="${RETEST_ROOT}/dongying_g1q3_issue_list.sync_manifest.json"
|
||||
fi
|
||||
fi
|
||||
|
||||
CMD=(
|
||||
"${PYTHON_BIN}" "${PROJECT_ROOT}/tools/feishu_project/sync_issue_data.py"
|
||||
--project-key "${PROJECT_KEY}"
|
||||
--user-key "${USER_KEY}"
|
||||
--view-name "${VIEW_NAME}"
|
||||
--work-item-type "${WORK_ITEM_TYPE}"
|
||||
--output-json "${EXPORT_JSON}"
|
||||
--sync-manifest-path "${SYNC_MANIFEST_PATH}"
|
||||
--snapshot-dir "${SNAPSHOT_DIR}"
|
||||
--python-bin "${PYTHON_BIN}"
|
||||
--download-root "${DOWNLOAD_ROOT}"
|
||||
--download-manifest-path "${DOWNLOAD_MANIFEST_PATH}"
|
||||
--inference-root "${INFERENCE_ROOT}"
|
||||
--inference-manifest-path "${INFERENCE_MANIFEST_PATH}"
|
||||
)
|
||||
|
||||
if [[ "${RUN_DOWNLOAD}" == "1" ]]; then
|
||||
CMD+=(--run-download)
|
||||
fi
|
||||
|
||||
if [[ "${RUN_INFERENCE}" == "1" ]]; then
|
||||
CMD+=(--run-inference)
|
||||
fi
|
||||
|
||||
if [[ "${USE_ISSUE_FRAME_WINDOW}" == "1" ]]; then
|
||||
CMD+=(--use-issue-frame-window --frame-before "${FRAME_BEFORE}" --frame-after "${FRAME_AFTER}" --missing-issue-frame-policy "${MISSING_ISSUE_FRAME_POLICY}")
|
||||
fi
|
||||
|
||||
if [[ "${ENABLE_TRACKING}" == "1" ]]; then
|
||||
CMD+=(--run-tracking --issue-tracking-script "${ISSUE_TRACKING_SCRIPT}")
|
||||
fi
|
||||
|
||||
if [[ "${SAVE_SNAPSHOT}" == "1" ]]; then
|
||||
CMD+=(--save-snapshot)
|
||||
fi
|
||||
|
||||
if [[ "${REFRESH_CHANGED_ISSUES}" == "1" ]]; then
|
||||
CMD+=(--refresh-changed-issues)
|
||||
fi
|
||||
|
||||
if [[ "${SKIP_EXISTING_INFERENCE}" == "1" ]]; then
|
||||
CMD+=(--skip-existing-inference)
|
||||
fi
|
||||
|
||||
if [[ "${DRY_RUN}" == "1" ]]; then
|
||||
CMD+=(--dry-run)
|
||||
fi
|
||||
|
||||
if [[ -n "${ISSUE_ID_MIN}" ]]; then
|
||||
CMD+=(--issue-id-min "${ISSUE_ID_MIN}")
|
||||
fi
|
||||
|
||||
if [[ -n "${ISSUE_ID_MAX}" ]]; then
|
||||
CMD+=(--issue-id-max "${ISSUE_ID_MAX}")
|
||||
fi
|
||||
|
||||
if [[ -n "${EXPORTED_MODEL}" ]]; then
|
||||
CMD+=(--exported-model "${EXPORTED_MODEL}")
|
||||
fi
|
||||
|
||||
if [[ -n "${TRACKING_MODEL_VERSION}" ]]; then
|
||||
CMD+=(--tracking-model-version "${TRACKING_MODEL_VERSION}")
|
||||
fi
|
||||
|
||||
if [[ "${SHOW_DISTANCE_LABEL}" == "1" ]]; then
|
||||
CMD+=("--inference-arg=--show-distance-label")
|
||||
if [[ -n "${DISTANCE_LABEL_MODE}" ]]; then
|
||||
CMD+=("--inference-arg=--distance-label-mode" "--inference-arg=${DISTANCE_LABEL_MODE}")
|
||||
fi
|
||||
if [[ -n "${DISTANCE_LABEL_PANELS}" ]]; then
|
||||
# shellcheck disable=SC2206
|
||||
DISTANCE_LABEL_PANELS_ARR=(${DISTANCE_LABEL_PANELS})
|
||||
CMD+=("--inference-arg=--distance-label-panels")
|
||||
for panel in "${DISTANCE_LABEL_PANELS_ARR[@]}"; do
|
||||
CMD+=("--inference-arg=${panel}")
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "${ENABLE_RETEST}" == "1" ]] && [[ -n "${RETEST_NAME_KEYWORDS}" ]]; then
|
||||
IFS=',' read -r -a RETEST_KEYWORD_ARRAY <<< "${RETEST_NAME_KEYWORDS}"
|
||||
for keyword in "${RETEST_KEYWORD_ARRAY[@]}"; do
|
||||
keyword="${keyword#"${keyword%%[![:space:]]*}"}"
|
||||
keyword="${keyword%"${keyword##*[![:space:]]}"}"
|
||||
if [[ -n "${keyword}" ]]; then
|
||||
CMD+=(--issue-name-keyword "${keyword}")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
CMD+=("$@")
|
||||
"${CMD[@]}"
|
||||
1582
tools/feishu_project/sync_issue_data.py
Executable file
1582
tools/feishu_project/sync_issue_data.py
Executable file
File diff suppressed because it is too large
Load Diff
5
tools/model_inference/.gitignore
vendored
Executable file
5
tools/model_inference/.gitignore
vendored
Executable file
@@ -0,0 +1,5 @@
|
||||
.cache/
|
||||
__pycache__/
|
||||
core/__pycache__/
|
||||
adapters/__pycache__/
|
||||
data_tools/__pycache__/
|
||||
189
tools/model_inference/README.md
Executable file
189
tools/model_inference/README.md
Executable file
@@ -0,0 +1,189 @@
|
||||
# Two-ROI Exported Model Inference
|
||||
|
||||
`tools/model_inference` contains a self-contained inference pipeline for the exported two-ROI ONNX or TorchScript model.
|
||||
|
||||
## Layout
|
||||
|
||||
- `run_two_roi_exported_onnx_infer.py`
|
||||
Compatibility entry point kept at the original path.
|
||||
- `core/`
|
||||
Core inference pipeline, decode logic, geometry helpers, and shared types.
|
||||
- `adapters/`
|
||||
Input-source adapters for video directories, PDCL clip exports, and event-id resolution.
|
||||
- `scripts/`
|
||||
Shell launchers grouped by usage mode.
|
||||
- `data_tools/`
|
||||
Small preprocessing helpers for CSV/XLSX conversion.
|
||||
- `docs/`
|
||||
Design notes and usage background documents.
|
||||
- `examples/`
|
||||
Sample JSON/CSV/XLSX/txt inputs used by the helper scripts.
|
||||
|
||||
## Files
|
||||
|
||||
- `core/run_two_roi_exported_onnx_infer.py`
|
||||
Main implementation. Reads one clip-export directory, runs two-ROI ONNX or TorchScript inference, decodes 2D/3D results, and saves visualizations plus `predictions.json`.
|
||||
- `core/two_roi_infer_utils.py`
|
||||
Minimal local utilities for ROI crop, calibration handling, 2D decode, top-k selection, and common serialization helpers.
|
||||
- `core/two_roi_3d_utils.py`
|
||||
Minimal local 3D geometry, projection, yaw decoding, and 3D drawing helpers.
|
||||
- `scripts/run_two_roi_exported_onnx_infer.sh`
|
||||
Example shell wrapper.
|
||||
|
||||
## External Dependencies
|
||||
|
||||
This package does not depend on `ultralytics` at runtime.
|
||||
|
||||
Required Python packages:
|
||||
|
||||
- `numpy`
|
||||
- `opencv-python`
|
||||
- `pyyaml`
|
||||
- `onnxruntime` for `.onnx` models
|
||||
- `torch` for `.torchscript` models
|
||||
|
||||
## Expected Input Layout
|
||||
|
||||
The script can take a clip-export directory directly.
|
||||
|
||||
Expected structure:
|
||||
|
||||
```text
|
||||
clip_export_xxx/
|
||||
├── images/
|
||||
│ ├── *.png
|
||||
│ └── ...
|
||||
├── calib/
|
||||
│ └── L2_calib/
|
||||
│ └── camera4.json
|
||||
├── manifest.json
|
||||
└── calib_summary.json
|
||||
```
|
||||
|
||||
The script automatically reads:
|
||||
|
||||
- images from `images/`
|
||||
- calibration from `calib/L2_calib/camera4.json` or `calib/camera4.json`
|
||||
|
||||
## Expected Exported Model Outputs
|
||||
|
||||
The exported model should be the raw-head merged artifact produced by `tools/model_merging/merge_models_of_2roi_yolo26.py`. The same output contract is used for both ONNX and TorchScript.
|
||||
|
||||
Required output tensor names:
|
||||
|
||||
- `roi0_boxes_head_raw`
|
||||
- `roi0_scores_head_raw`
|
||||
- `roi0_preds_3d_head_raw`
|
||||
- `roi1_boxes_head_raw`
|
||||
- `roi1_scores_head_raw`
|
||||
- `roi1_preds_3d_head_raw`
|
||||
|
||||
Optional output tensor names:
|
||||
|
||||
- `roi0_preds_edge_head_raw`
|
||||
- `roi1_preds_edge_head_raw`
|
||||
|
||||
If the merged model is exported with `--edge-head-mode drop`, the runtime keeps the
|
||||
same 2D/3D decode path and automatically disables edge-yaw reconstruction.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```bash
|
||||
python tools/model_inference/run_two_roi_exported_onnx_infer.py \
|
||||
--case-dir tools/pdcl_inference/clip_exports/clip_export_G1M3_G1Q3_6284_019cb7f4-a944-7c22-5427-5b75b25545c7 \
|
||||
--exported-model runs/export/train_mono3d_two_roi_202603251430/merged_model.onnx \
|
||||
--output-dir /tmp/two_roi_exported_model_run
|
||||
```
|
||||
|
||||
For CNCAP JSON batch video inference:
|
||||
|
||||
```bash
|
||||
python tools/model_inference/run_two_roi_exported_onnx_infer.py \
|
||||
--cncap-json-file tools/model_inference/examples/cncap/G1M3_AFS1616_CNCAP-202411.json \
|
||||
--cncap-path-prefix-src /mnt/hfs/project-G1M3 \
|
||||
--cncap-path-prefix-dst /mnt/G1M3 \
|
||||
--exported-model runs/export/train_mono3d_two_roi_20260403-raw-fuse/merged_model.onnx \
|
||||
--output-dir /tmp/two_roi_exported_model_cncap_run
|
||||
```
|
||||
|
||||
## Shell Wrapper
|
||||
|
||||
```bash
|
||||
bash tools/model_inference/scripts/run_two_roi_exported_onnx_infer.sh
|
||||
```
|
||||
|
||||
Update the paths in the shell script before handing it to downstream users if needed.
|
||||
|
||||
## Important Arguments
|
||||
|
||||
- `--case-dir`
|
||||
Clip-export directory containing `images/` and either `calib/L2_calib/camera4.json` or `calib/camera4.json`.
|
||||
- `--cncap-json-file`
|
||||
CNCAP JSON file containing `values` entries that point to `sigmastar.1` directories. The script rewrites each mounted path prefix, resolves `camera4.bin` plus `test_data/calibs/camera4.json`, and then reuses the video-case inference flow.
|
||||
- `--exported-model`
|
||||
Merged raw-head exported model path. Supports `.onnx` and `.torchscript`.
|
||||
- `--output-dir`
|
||||
Directory used to save visualization images and `predictions.json`.
|
||||
- `--roi0-model`, `--roi1-model`
|
||||
Training checkpoints used only for metadata-free ROI preset alignment are no longer required by any external framework, but are still used as plain path fields in the current CLI contract. Keep them aligned with your deployment pair.
|
||||
- `--roi0-roi`, `--roi1-roi`
|
||||
ROI crop sizes before resize.
|
||||
- `--roi0-imgsz`, `--roi1-imgsz`
|
||||
ROI input tensor sizes used by the exported model. If omitted, the script first tries the export manifest.
|
||||
- `--classes`
|
||||
Optional class-id filter.
|
||||
- `--max-images`
|
||||
Limit the number of images for quick smoke tests.
|
||||
- `--providers`
|
||||
Optional ONNX Runtime providers, for example `CUDAExecutionProvider CPUExecutionProvider`. Only used for `.onnx` models.
|
||||
|
||||
## Outputs
|
||||
|
||||
The script writes:
|
||||
|
||||
- one visualization image per input frame
|
||||
- `predictions.json`
|
||||
|
||||
`predictions.json` contains per-frame, per-ROI prediction records including:
|
||||
|
||||
- 2D box
|
||||
- confidence
|
||||
- class id and class name
|
||||
- yaw
|
||||
- edge-yaw diagnostics
|
||||
- decoded 3D center
|
||||
- ROI crop bounds
|
||||
|
||||
## Notes
|
||||
|
||||
- This pipeline intentionally runs decode and postprocess outside the exported graph.
|
||||
- It is useful for downstream deployment and migration because the runtime path only depends on common Python packages.
|
||||
- If the exported model export mode changes, make sure the output tensor names still match the names listed above.
|
||||
|
||||
## Known Residuals
|
||||
|
||||
Validation against the batch PyTorch reference path
|
||||
`tools/pdcl_inference/two_roi_inference.py` on the first 20 frames of
|
||||
`clip_export_G1M3_G1Q3_6284_019cb7f4-a944-7c22-5427-5b75b25545c7`
|
||||
shows that the self-contained ONNX path matches the 3D branch decisions
|
||||
after bbox-based matching:
|
||||
|
||||
- `visible_face_type` mismatch: `0`
|
||||
- `visible_face_types` mismatch: `0`
|
||||
- `edge_yaw_confident` mismatch: `0`
|
||||
|
||||
Two near-threshold count mismatches are still treated as known residuals.
|
||||
In both cases the PyTorch batch path keeps one extra `cls_id=6` detection
|
||||
with confidence just above the `0.25` threshold, while the ONNX path drops it:
|
||||
|
||||
- `019cb7f4-a944-7c22-5427-5b75b25545c7_80364.png`, `roi0`
|
||||
Batch-only detection: `conf=0.252197`
|
||||
- `019cb7f4-a944-7c22-5427-5b75b25545c7_80370.png`, `roi0`
|
||||
Batch-only detection: `conf=0.251094`
|
||||
|
||||
Current interpretation:
|
||||
|
||||
- These residuals are consistent with small ONNX vs PyTorch numerical drift
|
||||
around the confidence threshold.
|
||||
- The implementation is intentionally kept unchanged; no extra confidence
|
||||
epsilon is applied just to eliminate these edge cases.
|
||||
1
tools/model_inference/__init__.py
Executable file
1
tools/model_inference/__init__.py
Executable file
@@ -0,0 +1 @@
|
||||
"""Two-ROI model inference package."""
|
||||
1
tools/model_inference/adapters/__init__.py
Executable file
1
tools/model_inference/adapters/__init__.py
Executable file
@@ -0,0 +1 @@
|
||||
"""Input adapters for model_inference batch sources."""
|
||||
822
tools/model_inference/adapters/eventid_clip_resolver.py
Executable file
822
tools/model_inference/adapters/eventid_clip_resolver.py
Executable file
@@ -0,0 +1,822 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
import re
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
try:
|
||||
from .get_clip_by_eventid import get_associated_clip_ids
|
||||
from .pdcl_clip_export_utils import (
|
||||
build_clip_tasks_from_clip_ids,
|
||||
run_clip_tasks_inference_exported,
|
||||
)
|
||||
except ImportError:
|
||||
from get_clip_by_eventid import get_associated_clip_ids
|
||||
from pdcl_clip_export_utils import (
|
||||
build_clip_tasks_from_clip_ids,
|
||||
run_clip_tasks_inference_exported,
|
||||
)
|
||||
|
||||
DEFAULT_EVENT_CACHE_FILE = Path(__file__).resolve().parents[1] / ".cache" / "event_clip_cache.json"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ResolvedEventRecord:
|
||||
scene: str
|
||||
record_index: int
|
||||
event_id: str
|
||||
event_id_field_used: str
|
||||
source_record: dict[str, Any]
|
||||
clip_ids: list[str]
|
||||
clip_source: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EventResolutionStats:
|
||||
total_events: int
|
||||
cache_hits: int
|
||||
cache_misses: int
|
||||
request_workers: int
|
||||
cache_file: str
|
||||
direct_clip_records: int = 0
|
||||
event_lookup_records: int = 0
|
||||
|
||||
|
||||
def _dedupe_preserve_order(values: list[str]) -> list[str]:
|
||||
ordered: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for value in values:
|
||||
token = str(value).strip()
|
||||
if not token or token in seen:
|
||||
continue
|
||||
seen.add(token)
|
||||
ordered.append(token)
|
||||
return ordered
|
||||
|
||||
|
||||
def _sanitize_identifier_for_path(identifier: str, prefix: str = "event_id") -> str:
|
||||
token = re.sub(r'[\\/:*?"<>|\s]+', "_", str(identifier or "").strip())
|
||||
token = token.strip("._")
|
||||
token = token or "unknown_id"
|
||||
normalized_prefix = re.sub(r"[^0-9A-Za-z]+", "_", str(prefix or "").strip())
|
||||
normalized_prefix = normalized_prefix.strip("._") or "id"
|
||||
return f"{normalized_prefix}_{token}"
|
||||
|
||||
|
||||
def _build_resolution_stats_payload(resolution_stats: "EventResolutionStats") -> dict[str, Any]:
|
||||
return {
|
||||
"total_events": resolution_stats.total_events,
|
||||
"cache_hits": resolution_stats.cache_hits,
|
||||
"cache_misses": resolution_stats.cache_misses,
|
||||
"request_workers": resolution_stats.request_workers,
|
||||
"cache_file": resolution_stats.cache_file,
|
||||
"direct_clip_records": resolution_stats.direct_clip_records,
|
||||
"event_lookup_records": resolution_stats.event_lookup_records,
|
||||
}
|
||||
|
||||
|
||||
def _record_key(record: "ResolvedEventRecord") -> tuple[str, int, str]:
|
||||
return (record.scene, int(record.record_index), str(record.event_id))
|
||||
|
||||
|
||||
def _normalize_direct_clip_ids(raw_value: Any) -> list[str]:
|
||||
if isinstance(raw_value, list):
|
||||
return _dedupe_preserve_order([str(item).strip() for item in raw_value if str(item).strip()])
|
||||
|
||||
if isinstance(raw_value, str):
|
||||
text = raw_value.strip()
|
||||
if not text:
|
||||
return []
|
||||
if text.startswith("["):
|
||||
try:
|
||||
parsed = json.loads(text)
|
||||
except Exception:
|
||||
parsed = None
|
||||
if isinstance(parsed, list):
|
||||
return _dedupe_preserve_order([str(item).strip() for item in parsed if str(item).strip()])
|
||||
return _dedupe_preserve_order([token for token in re.split(r"[\s,]+", text) if token])
|
||||
|
||||
if raw_value is None:
|
||||
return []
|
||||
|
||||
token = str(raw_value).strip()
|
||||
if not token:
|
||||
return []
|
||||
return [token]
|
||||
|
||||
|
||||
def _extract_direct_clip_ids(record: dict[str, Any], clip_ids_field: str) -> tuple[bool, list[str]]:
|
||||
field_name = str(clip_ids_field or "").strip()
|
||||
if not field_name or field_name not in record:
|
||||
return False, []
|
||||
return True, _normalize_direct_clip_ids(record.get(field_name))
|
||||
|
||||
|
||||
def _resolve_record_identifier(
|
||||
record: dict[str, Any],
|
||||
preferred_field: str,
|
||||
*,
|
||||
allow_direct_clip_fallback: bool = False,
|
||||
) -> tuple[str, str]:
|
||||
candidate_fields: list[str] = []
|
||||
preferred_token = str(preferred_field or "").strip()
|
||||
if preferred_token:
|
||||
candidate_fields.append(preferred_token)
|
||||
|
||||
if allow_direct_clip_fallback:
|
||||
for field_name in ("rawid", "event_id", "data_path"):
|
||||
if field_name not in candidate_fields:
|
||||
candidate_fields.append(field_name)
|
||||
|
||||
for field_name in candidate_fields:
|
||||
identifier = str(record.get(field_name, "")).strip()
|
||||
if identifier:
|
||||
return identifier, field_name
|
||||
|
||||
return "", preferred_token
|
||||
|
||||
|
||||
def _extract_condition_values(source_record: dict[str, Any], condition_fields: list[str]) -> dict[str, str]:
|
||||
return {
|
||||
str(field): str(source_record.get(field, "")).strip()
|
||||
for field in condition_fields
|
||||
}
|
||||
|
||||
|
||||
def _select_event_records_by_condition(
|
||||
records: list["ResolvedEventRecord"],
|
||||
*,
|
||||
condition_fields: list[str],
|
||||
max_records_per_condition: int,
|
||||
selection_strategy: str,
|
||||
) -> tuple[list["ResolvedEventRecord"], dict[str, Any]]:
|
||||
normalized_fields = [str(field).strip() for field in condition_fields if str(field).strip()]
|
||||
limit = max(0, int(max_records_per_condition))
|
||||
selection_enabled = bool(normalized_fields and limit > 0)
|
||||
summary: dict[str, Any] = {
|
||||
"enabled": selection_enabled,
|
||||
"condition_fields": normalized_fields,
|
||||
"max_records_per_condition": limit,
|
||||
"selection_strategy": selection_strategy,
|
||||
"records_before_selection": len(records),
|
||||
"records_after_selection": len(records),
|
||||
"group_count": 0,
|
||||
"groups": [],
|
||||
}
|
||||
if not selection_enabled:
|
||||
return list(records), summary
|
||||
|
||||
if selection_strategy != "first":
|
||||
raise ValueError(f"Unsupported condition selection strategy: {selection_strategy!r}")
|
||||
|
||||
selected_records: list[ResolvedEventRecord] = []
|
||||
selected_counts: dict[tuple[str, tuple[str, ...]], int] = {}
|
||||
group_summaries: dict[tuple[str, tuple[str, ...]], dict[str, Any]] = {}
|
||||
|
||||
for record in records:
|
||||
condition_values = _extract_condition_values(record.source_record, normalized_fields)
|
||||
condition_tuple = tuple(condition_values[field] for field in normalized_fields)
|
||||
group_key = (record.scene, condition_tuple)
|
||||
group_summary = group_summaries.setdefault(
|
||||
group_key,
|
||||
{
|
||||
"scene": record.scene,
|
||||
"condition_values": condition_values,
|
||||
"record_count": 0,
|
||||
"selected_count": 0,
|
||||
"skipped_count": 0,
|
||||
"selected_record_ids": [],
|
||||
"selected_record_indices": [],
|
||||
"skipped_record_ids": [],
|
||||
"skipped_record_indices": [],
|
||||
},
|
||||
)
|
||||
group_summary["record_count"] += 1
|
||||
current_selected_count = selected_counts.get(group_key, 0)
|
||||
|
||||
if current_selected_count < limit:
|
||||
selected_records.append(record)
|
||||
selected_counts[group_key] = current_selected_count + 1
|
||||
group_summary["selected_count"] += 1
|
||||
group_summary["selected_record_ids"].append(record.event_id)
|
||||
group_summary["selected_record_indices"].append(record.record_index)
|
||||
continue
|
||||
|
||||
group_summary["skipped_count"] += 1
|
||||
group_summary["skipped_record_ids"].append(record.event_id)
|
||||
group_summary["skipped_record_indices"].append(record.record_index)
|
||||
|
||||
summary["records_after_selection"] = len(selected_records)
|
||||
summary["group_count"] = len(group_summaries)
|
||||
summary["groups"] = list(group_summaries.values())
|
||||
return selected_records, summary
|
||||
|
||||
|
||||
def _filter_selection_summary_for_scene(selection_summary: dict[str, Any], scene: str) -> dict[str, Any]:
|
||||
if not selection_summary:
|
||||
return {}
|
||||
|
||||
scene_groups = [
|
||||
dict(group)
|
||||
for group in selection_summary.get("groups", [])
|
||||
if str(group.get("scene", "")) == str(scene)
|
||||
]
|
||||
filtered = dict(selection_summary)
|
||||
filtered["groups"] = scene_groups
|
||||
filtered["group_count"] = len(scene_groups)
|
||||
if scene_groups:
|
||||
filtered["records_before_selection"] = sum(int(group.get("record_count", 0)) for group in scene_groups)
|
||||
filtered["records_after_selection"] = sum(int(group.get("selected_count", 0)) for group in scene_groups)
|
||||
else:
|
||||
filtered["records_before_selection"] = 0
|
||||
filtered["records_after_selection"] = 0
|
||||
return filtered
|
||||
|
||||
|
||||
def _format_condition_summary(source_record: dict[str, Any], condition_fields: list[str]) -> str:
|
||||
normalized_fields = [str(field).strip() for field in condition_fields if str(field).strip()]
|
||||
if not normalized_fields:
|
||||
return ""
|
||||
values = _extract_condition_values(source_record, normalized_fields)
|
||||
return ", ".join(f"{field}={values.get(field, '')}" for field in normalized_fields)
|
||||
|
||||
|
||||
def _print_event_json_summary(
|
||||
*,
|
||||
args: argparse.Namespace,
|
||||
manifest_path: Path,
|
||||
resolved_records: list["ResolvedEventRecord"],
|
||||
selected_records: list["ResolvedEventRecord"],
|
||||
selection_summary: dict[str, Any],
|
||||
scene_results: dict[str, dict[str, Any]],
|
||||
) -> None:
|
||||
print(f"Event JSON file: {args.event_json_file}")
|
||||
if args.scene:
|
||||
print(f"Scene filter: {args.scene}")
|
||||
print(
|
||||
"Resolved records: "
|
||||
f"{len(resolved_records)}, selected records: {len(selected_records)}"
|
||||
)
|
||||
print(f"Manifest: {manifest_path}")
|
||||
|
||||
if not selected_records:
|
||||
print("No records selected.")
|
||||
return
|
||||
|
||||
condition_fields = [str(field).strip() for field in getattr(args, "condition_fields", []) if str(field).strip()]
|
||||
if args.selection_only:
|
||||
print("Selection-only mode: no clip export or inference was run.")
|
||||
elif selection_summary.get("enabled"):
|
||||
print(
|
||||
"Condition selection: "
|
||||
f"{selection_summary.get('group_count', 0)} groups, "
|
||||
f"strategy={selection_summary.get('selection_strategy', '')}, "
|
||||
f"max_records_per_condition={selection_summary.get('max_records_per_condition', 0)}"
|
||||
)
|
||||
|
||||
for scene in sorted(scene_results):
|
||||
scene_result = scene_results[scene]
|
||||
print(
|
||||
f"Scene {scene}: selected_record_count="
|
||||
f"{scene_result.get('selected_record_count', len([record for record in selected_records if record.scene == scene]))}"
|
||||
)
|
||||
scene_manifest_path = scene_result.get("scene_manifest_path")
|
||||
if scene_manifest_path:
|
||||
print(f" Scene manifest: {scene_manifest_path}")
|
||||
if scene_result.get("scene_output_dir"):
|
||||
print(f" Output dir: {scene_result['scene_output_dir']}")
|
||||
|
||||
scene_records = [record for record in selected_records if record.scene == scene]
|
||||
for record in scene_records:
|
||||
condition_summary = _format_condition_summary(record.source_record, condition_fields)
|
||||
summary_parts = []
|
||||
if condition_summary:
|
||||
summary_parts.append(condition_summary)
|
||||
summary_parts.append(f"record_id={record.event_id}")
|
||||
summary_parts.append(f"clips={len(record.clip_ids)}")
|
||||
print(f" - {'; '.join(summary_parts)}")
|
||||
|
||||
|
||||
def _build_scene_manifest_payload(
|
||||
*,
|
||||
args: argparse.Namespace,
|
||||
scene: str,
|
||||
scene_records: list["ResolvedEventRecord"],
|
||||
resolution_stats: "EventResolutionStats",
|
||||
selection_summary: Optional[dict[str, Any]] = None,
|
||||
) -> dict[str, Any]:
|
||||
payload = {
|
||||
"event_json_file": args.event_json_file,
|
||||
"scene": scene,
|
||||
"event_id_field": args.event_id_field,
|
||||
"event_clip_ids_field": args.event_clip_ids_field,
|
||||
"resolution_stats": _build_resolution_stats_payload(resolution_stats),
|
||||
"event_record_count": len(scene_records),
|
||||
"records": [
|
||||
{
|
||||
"record_index": record.record_index,
|
||||
"event_id": record.event_id,
|
||||
"event_id_field_used": record.event_id_field_used,
|
||||
"clip_ids": record.clip_ids,
|
||||
"clip_source": record.clip_source,
|
||||
"source_record": record.source_record,
|
||||
}
|
||||
for record in scene_records
|
||||
],
|
||||
}
|
||||
if selection_summary is not None:
|
||||
payload["selection"] = selection_summary
|
||||
return payload
|
||||
|
||||
|
||||
def _build_event_manifest_payload(
|
||||
*,
|
||||
args: argparse.Namespace,
|
||||
scene: str,
|
||||
event_id: str,
|
||||
event_records: list["ResolvedEventRecord"],
|
||||
clip_ids: list[str],
|
||||
resolution_stats: "EventResolutionStats",
|
||||
) -> dict[str, Any]:
|
||||
condition_fields = [str(field).strip() for field in getattr(args, "condition_fields", []) if str(field).strip()]
|
||||
payload = {
|
||||
"event_json_file": args.event_json_file,
|
||||
"scene": scene,
|
||||
"event_id_field": args.event_id_field,
|
||||
"event_clip_ids_field": args.event_clip_ids_field,
|
||||
"event_id": event_id,
|
||||
"resolution_stats": _build_resolution_stats_payload(resolution_stats),
|
||||
"event_record_count": len(event_records),
|
||||
"clip_ids": clip_ids,
|
||||
"clip_count": len(clip_ids),
|
||||
"records": [
|
||||
{
|
||||
"record_index": record.record_index,
|
||||
"event_id": record.event_id,
|
||||
"event_id_field_used": record.event_id_field_used,
|
||||
"clip_ids": record.clip_ids,
|
||||
"clip_source": record.clip_source,
|
||||
"source_record": record.source_record,
|
||||
}
|
||||
for record in event_records
|
||||
],
|
||||
}
|
||||
if event_records:
|
||||
payload["event_id_field_used"] = event_records[0].event_id_field_used
|
||||
payload["clip_source"] = sorted({record.clip_source for record in event_records})
|
||||
if condition_fields and event_records:
|
||||
payload["condition_values"] = _extract_condition_values(event_records[0].source_record, condition_fields)
|
||||
return payload
|
||||
|
||||
|
||||
def load_event_scene_json(json_file: str) -> dict[str, list[dict[str, Any]]]:
|
||||
path = Path(json_file)
|
||||
with path.open("r", encoding="utf-8") as file:
|
||||
payload = json.load(file)
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError(f"Expected top-level dict in {path}, got {type(payload).__name__}")
|
||||
|
||||
normalized: dict[str, list[dict[str, Any]]] = {}
|
||||
for scene, records in payload.items():
|
||||
if not isinstance(records, list):
|
||||
raise ValueError(f"Scene {scene!r} should map to a list, got {type(records).__name__}")
|
||||
normalized[str(scene)] = [dict(item) if isinstance(item, dict) else {"value": item} for item in records]
|
||||
return normalized
|
||||
|
||||
|
||||
def load_event_clip_cache(cache_file: str | Path) -> dict[str, list[str]]:
|
||||
cache_path = Path(cache_file)
|
||||
if not cache_path.exists():
|
||||
return {}
|
||||
|
||||
try:
|
||||
payload = json.loads(cache_path.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
if not isinstance(payload, dict):
|
||||
return {}
|
||||
|
||||
cache: dict[str, list[str]] = {}
|
||||
for event_id, clip_ids in payload.items():
|
||||
if isinstance(clip_ids, list):
|
||||
cache[str(event_id)] = [str(item).strip() for item in clip_ids if str(item).strip()]
|
||||
return cache
|
||||
|
||||
|
||||
def save_event_clip_cache(cache_file: str | Path, cache_payload: dict[str, list[str]]) -> Path:
|
||||
cache_path = Path(cache_file)
|
||||
cache_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
normalized = {
|
||||
str(event_id): [str(item).strip() for item in clip_ids if str(item).strip()]
|
||||
for event_id, clip_ids in cache_payload.items()
|
||||
}
|
||||
with cache_path.open("w", encoding="utf-8") as file:
|
||||
json.dump(normalized, file, indent=2, ensure_ascii=False)
|
||||
return cache_path
|
||||
|
||||
|
||||
def resolve_event_clip_ids(
|
||||
event_ids: list[str],
|
||||
*,
|
||||
timeout: float = 60.0,
|
||||
cache_file: str | Path = DEFAULT_EVENT_CACHE_FILE,
|
||||
workers: int = 4,
|
||||
max_retries: int = 3,
|
||||
retry_backoff_sec: float = 2.0,
|
||||
) -> tuple[dict[str, list[str]], EventResolutionStats]:
|
||||
ordered_event_ids: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for event_id in event_ids:
|
||||
normalized = str(event_id).strip()
|
||||
if not normalized or normalized in seen:
|
||||
continue
|
||||
seen.add(normalized)
|
||||
ordered_event_ids.append(normalized)
|
||||
|
||||
cache_payload = load_event_clip_cache(cache_file)
|
||||
resolved: dict[str, list[str]] = {}
|
||||
unresolved_event_ids: list[str] = []
|
||||
cache_hits = 0
|
||||
cache_misses = 0
|
||||
|
||||
for event_id in ordered_event_ids:
|
||||
if event_id in cache_payload:
|
||||
resolved[event_id] = list(cache_payload[event_id])
|
||||
cache_hits += 1
|
||||
else:
|
||||
unresolved_event_ids.append(event_id)
|
||||
cache_misses += 1
|
||||
|
||||
if unresolved_event_ids:
|
||||
max_workers = max(1, min(int(workers), len(unresolved_event_ids)))
|
||||
if max_workers == 1:
|
||||
for event_id in unresolved_event_ids:
|
||||
clip_ids = get_associated_clip_ids(
|
||||
event_id,
|
||||
timeout=timeout,
|
||||
max_retries=max_retries,
|
||||
retry_backoff_sec=retry_backoff_sec,
|
||||
)
|
||||
resolved[event_id] = clip_ids
|
||||
cache_payload[event_id] = clip_ids
|
||||
else:
|
||||
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||
future_to_event_id = {
|
||||
executor.submit(
|
||||
get_associated_clip_ids,
|
||||
event_id,
|
||||
timeout,
|
||||
max_retries,
|
||||
retry_backoff_sec,
|
||||
): event_id
|
||||
for event_id in unresolved_event_ids
|
||||
}
|
||||
for future in as_completed(future_to_event_id):
|
||||
event_id = future_to_event_id[future]
|
||||
clip_ids = future.result()
|
||||
resolved[event_id] = clip_ids
|
||||
cache_payload[event_id] = clip_ids
|
||||
save_event_clip_cache(cache_file, cache_payload)
|
||||
worker_count = max(1, min(int(workers), len(unresolved_event_ids)))
|
||||
else:
|
||||
worker_count = 0
|
||||
|
||||
stats = EventResolutionStats(
|
||||
total_events=len(ordered_event_ids),
|
||||
cache_hits=cache_hits,
|
||||
cache_misses=cache_misses,
|
||||
request_workers=worker_count,
|
||||
cache_file=str(Path(cache_file).resolve()),
|
||||
)
|
||||
return resolved, stats
|
||||
|
||||
|
||||
def resolve_event_records(
|
||||
json_file: str,
|
||||
scene_filter: Optional[str] = None,
|
||||
event_id_field: str = "data_path",
|
||||
clip_ids_field: str = "clips",
|
||||
max_events: int = 0,
|
||||
timeout: float = 60.0,
|
||||
cache_file: str | Path = DEFAULT_EVENT_CACHE_FILE,
|
||||
workers: int = 4,
|
||||
max_retries: int = 3,
|
||||
retry_backoff_sec: float = 2.0,
|
||||
) -> tuple[list[ResolvedEventRecord], EventResolutionStats]:
|
||||
scene_payload = load_event_scene_json(json_file)
|
||||
scene_names = [scene_filter] if scene_filter else list(scene_payload)
|
||||
pending_records: list[tuple[str, int, dict[str, Any], str, str, list[str], str]] = []
|
||||
lookup_event_ids: list[str] = []
|
||||
processed = 0
|
||||
direct_clip_records = 0
|
||||
event_lookup_records = 0
|
||||
|
||||
for scene in scene_names:
|
||||
records = scene_payload.get(scene)
|
||||
if records is None:
|
||||
raise ValueError(f"Scene {scene!r} not found in {json_file}")
|
||||
|
||||
for index, record in enumerate(records):
|
||||
has_direct_clip_ids, direct_clip_ids = _extract_direct_clip_ids(record, clip_ids_field)
|
||||
event_id, resolved_id_field = _resolve_record_identifier(
|
||||
record,
|
||||
event_id_field,
|
||||
allow_direct_clip_fallback=has_direct_clip_ids,
|
||||
)
|
||||
if not event_id:
|
||||
continue
|
||||
|
||||
clip_source = "direct_clip_ids_field" if has_direct_clip_ids else "event_lookup"
|
||||
if has_direct_clip_ids:
|
||||
direct_clip_records += 1
|
||||
else:
|
||||
event_lookup_records += 1
|
||||
lookup_event_ids.append(event_id)
|
||||
|
||||
pending_records.append(
|
||||
(scene, index, record, event_id, resolved_id_field, direct_clip_ids, clip_source)
|
||||
)
|
||||
processed += 1
|
||||
if max_events > 0 and processed >= max_events:
|
||||
break
|
||||
if max_events > 0 and processed >= max_events:
|
||||
break
|
||||
|
||||
resolved_clip_map: dict[str, list[str]] = {}
|
||||
lookup_stats = EventResolutionStats(
|
||||
total_events=0,
|
||||
cache_hits=0,
|
||||
cache_misses=0,
|
||||
request_workers=0,
|
||||
cache_file=str(Path(cache_file).resolve()),
|
||||
)
|
||||
if lookup_event_ids:
|
||||
resolved_clip_map, lookup_stats = resolve_event_clip_ids(
|
||||
lookup_event_ids,
|
||||
timeout=timeout,
|
||||
cache_file=cache_file,
|
||||
workers=workers,
|
||||
max_retries=max_retries,
|
||||
retry_backoff_sec=retry_backoff_sec,
|
||||
)
|
||||
|
||||
resolved = [
|
||||
ResolvedEventRecord(
|
||||
scene=scene,
|
||||
record_index=index,
|
||||
event_id=event_id,
|
||||
event_id_field_used=resolved_id_field,
|
||||
source_record=record,
|
||||
clip_ids=direct_clip_ids if clip_source == "direct_clip_ids_field" else resolved_clip_map.get(event_id, []),
|
||||
clip_source=clip_source,
|
||||
)
|
||||
for scene, index, record, event_id, resolved_id_field, direct_clip_ids, clip_source in pending_records
|
||||
]
|
||||
stats = EventResolutionStats(
|
||||
total_events=len(pending_records),
|
||||
cache_hits=lookup_stats.cache_hits,
|
||||
cache_misses=lookup_stats.cache_misses,
|
||||
request_workers=lookup_stats.request_workers,
|
||||
cache_file=str(Path(cache_file).resolve()),
|
||||
direct_clip_records=direct_clip_records,
|
||||
event_lookup_records=event_lookup_records,
|
||||
)
|
||||
return resolved, stats
|
||||
|
||||
|
||||
def build_event_resolution_payload(
|
||||
json_file: str,
|
||||
resolved_records: list[ResolvedEventRecord],
|
||||
event_id_field: str,
|
||||
clip_ids_field: str,
|
||||
stats: EventResolutionStats,
|
||||
selection_summary: Optional[dict[str, Any]] = None,
|
||||
selected_record_keys: Optional[set[tuple[str, int, str]]] = None,
|
||||
) -> dict[str, Any]:
|
||||
selection_enabled = bool(selection_summary and selection_summary.get("enabled"))
|
||||
selected_keys = selected_record_keys or set()
|
||||
scenes: dict[str, list[dict[str, Any]]] = {}
|
||||
for record in resolved_records:
|
||||
scenes.setdefault(record.scene, []).append(
|
||||
{
|
||||
"record_index": record.record_index,
|
||||
"event_id_field": event_id_field,
|
||||
"event_id_field_used": record.event_id_field_used,
|
||||
"event_id": record.event_id,
|
||||
"event_clip_ids_field": clip_ids_field,
|
||||
"clip_ids": record.clip_ids,
|
||||
"clip_count": len(record.clip_ids),
|
||||
"clip_source": record.clip_source,
|
||||
"selected_for_inference": True if not selection_enabled else _record_key(record) in selected_keys,
|
||||
"source_record": record.source_record,
|
||||
}
|
||||
)
|
||||
|
||||
payload = {
|
||||
"event_json_file": json_file,
|
||||
"event_id_field": event_id_field,
|
||||
"event_clip_ids_field": clip_ids_field,
|
||||
"scene_count": len(scenes),
|
||||
"record_count": len(resolved_records),
|
||||
"selected_record_count": len(selected_keys) if selection_enabled else len(resolved_records),
|
||||
"resolution_stats": _build_resolution_stats_payload(stats),
|
||||
"scenes": scenes,
|
||||
}
|
||||
if selection_summary is not None:
|
||||
payload["selection"] = selection_summary
|
||||
return payload
|
||||
|
||||
|
||||
def save_event_resolution_manifest(
|
||||
output_root: Path,
|
||||
json_file: str,
|
||||
resolved_records: list[ResolvedEventRecord],
|
||||
event_id_field: str,
|
||||
clip_ids_field: str,
|
||||
stats: EventResolutionStats,
|
||||
selection_summary: Optional[dict[str, Any]] = None,
|
||||
selected_record_keys: Optional[set[tuple[str, int, str]]] = None,
|
||||
) -> Path:
|
||||
manifest_path = output_root / "_status" / "event_scene_manifest.json"
|
||||
manifest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
payload = build_event_resolution_payload(
|
||||
json_file,
|
||||
resolved_records,
|
||||
event_id_field,
|
||||
clip_ids_field,
|
||||
stats,
|
||||
selection_summary=selection_summary,
|
||||
selected_record_keys=selected_record_keys,
|
||||
)
|
||||
with manifest_path.open("w", encoding="utf-8") as file:
|
||||
json.dump(payload, file, indent=2, ensure_ascii=False)
|
||||
return manifest_path
|
||||
|
||||
|
||||
def run_event_json_inference_exported(
|
||||
*,
|
||||
context: Any,
|
||||
args: argparse.Namespace,
|
||||
load_env: Callable[[], Any],
|
||||
run_case_inference: Callable[..., dict[str, Any]],
|
||||
) -> dict[str, Any]:
|
||||
resolved_records, resolution_stats = resolve_event_records(
|
||||
json_file=args.event_json_file,
|
||||
scene_filter=args.scene,
|
||||
event_id_field=args.event_id_field,
|
||||
clip_ids_field=args.event_clip_ids_field,
|
||||
max_events=args.max_events,
|
||||
timeout=args.event_request_timeout,
|
||||
cache_file=args.event_cache_file,
|
||||
workers=args.event_resolve_workers,
|
||||
max_retries=args.event_request_retries,
|
||||
retry_backoff_sec=args.event_request_retry_backoff_sec,
|
||||
)
|
||||
selected_records, selection_summary = _select_event_records_by_condition(
|
||||
resolved_records,
|
||||
condition_fields=[str(field).strip() for field in getattr(args, "condition_fields", []) if str(field).strip()],
|
||||
max_records_per_condition=int(getattr(args, "max_records_per_condition", 0)),
|
||||
selection_strategy=str(getattr(args, "condition_select_strategy", "first")),
|
||||
)
|
||||
selected_record_keys = {_record_key(record) for record in selected_records}
|
||||
output_root = Path(args.output_dir).resolve()
|
||||
output_root.mkdir(parents=True, exist_ok=True)
|
||||
manifest_path = save_event_resolution_manifest(
|
||||
output_root=output_root,
|
||||
json_file=args.event_json_file,
|
||||
resolved_records=resolved_records,
|
||||
event_id_field=args.event_id_field,
|
||||
clip_ids_field=args.event_clip_ids_field,
|
||||
stats=resolution_stats,
|
||||
selection_summary=selection_summary,
|
||||
selected_record_keys=selected_record_keys,
|
||||
)
|
||||
|
||||
summary_by_scene: dict[str, dict[str, Any]] = {}
|
||||
for scene in sorted({record.scene for record in selected_records}):
|
||||
scene_records = [record for record in selected_records if record.scene == scene]
|
||||
scene_selection_summary = _filter_selection_summary_for_scene(selection_summary, scene)
|
||||
scene_export_root = Path(args.export_root).resolve() / scene
|
||||
scene_output_root = output_root / scene
|
||||
scene_output_root.mkdir(parents=True, exist_ok=True)
|
||||
scene_manifest_path = scene_output_root / "_status" / "scene_event_manifest.json"
|
||||
scene_manifest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with scene_manifest_path.open("w", encoding="utf-8") as file:
|
||||
json.dump(
|
||||
_build_scene_manifest_payload(
|
||||
args=args,
|
||||
scene=scene,
|
||||
scene_records=scene_records,
|
||||
resolution_stats=resolution_stats,
|
||||
selection_summary=scene_selection_summary,
|
||||
),
|
||||
file,
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
if args.selection_only:
|
||||
summary_by_scene[scene] = {
|
||||
"scene_output_dir": str(scene_output_root),
|
||||
"scene_manifest_path": str(scene_manifest_path),
|
||||
"selected_record_count": len(scene_records),
|
||||
"selection_only": True,
|
||||
}
|
||||
continue
|
||||
|
||||
event_records_by_id: dict[str, list[ResolvedEventRecord]] = {}
|
||||
event_order: list[str] = []
|
||||
for record in scene_records:
|
||||
if record.event_id not in event_records_by_id:
|
||||
event_order.append(record.event_id)
|
||||
event_records_by_id.setdefault(record.event_id, []).append(record)
|
||||
|
||||
selected_scene_clip_ids: set[str] = set()
|
||||
event_results: dict[str, dict[str, Any]] = {}
|
||||
event_dirs: dict[str, str] = {}
|
||||
event_export_dirs: dict[str, str] = {}
|
||||
for event_id in event_order:
|
||||
event_records = event_records_by_id[event_id]
|
||||
event_clip_ids = _dedupe_preserve_order(
|
||||
[clip_id for record in event_records for clip_id in record.clip_ids]
|
||||
)
|
||||
if args.limit_clips > 0:
|
||||
limited_clip_ids: list[str] = []
|
||||
for clip_id in event_clip_ids:
|
||||
if clip_id in selected_scene_clip_ids:
|
||||
limited_clip_ids.append(clip_id)
|
||||
continue
|
||||
if len(selected_scene_clip_ids) >= args.limit_clips:
|
||||
continue
|
||||
selected_scene_clip_ids.add(clip_id)
|
||||
limited_clip_ids.append(clip_id)
|
||||
event_clip_ids = limited_clip_ids
|
||||
|
||||
clip_tasks = build_clip_tasks_from_clip_ids(event_clip_ids)
|
||||
dir_prefix = "event_id"
|
||||
if any(record.clip_source == "direct_clip_ids_field" for record in event_records):
|
||||
dir_prefix = event_records[0].event_id_field_used or args.event_id_field or "record_id"
|
||||
event_dir_name = _sanitize_identifier_for_path(event_id, prefix=dir_prefix)
|
||||
event_export_root = scene_export_root / event_dir_name
|
||||
event_output_root = scene_output_root / event_dir_name
|
||||
event_export_dirs[event_id] = str(event_export_root)
|
||||
event_dirs[event_id] = str(event_output_root)
|
||||
|
||||
def on_before_run(export_root: Path, inference_root: Path, tasks: list[Any], *, _event_id: str = event_id, _event_records: list[ResolvedEventRecord] = event_records, _event_clip_ids: list[str] = event_clip_ids) -> None:
|
||||
payload = _build_event_manifest_payload(
|
||||
args=args,
|
||||
scene=scene,
|
||||
event_id=_event_id,
|
||||
event_records=_event_records,
|
||||
clip_ids=_event_clip_ids,
|
||||
resolution_stats=resolution_stats,
|
||||
)
|
||||
manifest_path = inference_root / "_status" / "event_manifest.json"
|
||||
manifest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with manifest_path.open("w", encoding="utf-8") as file:
|
||||
json.dump(payload, file, indent=2, ensure_ascii=False)
|
||||
|
||||
event_results[event_id] = run_clip_tasks_inference_exported(
|
||||
context=context,
|
||||
args=args,
|
||||
clip_tasks=clip_tasks,
|
||||
load_env=load_env,
|
||||
run_case_inference=run_case_inference,
|
||||
export_root=event_export_root,
|
||||
output_root=event_output_root,
|
||||
on_before_run=on_before_run,
|
||||
)
|
||||
|
||||
summary_by_scene[scene] = {
|
||||
"scene_output_dir": str(scene_output_root),
|
||||
"scene_manifest_path": str(scene_manifest_path),
|
||||
"event_export_dirs": event_export_dirs,
|
||||
"event_dirs": event_dirs,
|
||||
"event_results": event_results,
|
||||
}
|
||||
|
||||
result = {
|
||||
"event_json_file": args.event_json_file,
|
||||
"scene": args.scene or "",
|
||||
"event_id_field": args.event_id_field,
|
||||
"event_clip_ids_field": args.event_clip_ids_field,
|
||||
"manifest_path": str(manifest_path),
|
||||
"selection": selection_summary,
|
||||
"selected_record_count": len(selected_records),
|
||||
"resolution_stats": _build_resolution_stats_payload(resolution_stats),
|
||||
"scene_results": summary_by_scene,
|
||||
}
|
||||
_print_event_json_summary(
|
||||
args=args,
|
||||
manifest_path=manifest_path,
|
||||
resolved_records=resolved_records,
|
||||
selected_records=selected_records,
|
||||
selection_summary=selection_summary,
|
||||
scene_results=summary_by_scene,
|
||||
)
|
||||
return result
|
||||
141
tools/model_inference/adapters/get_clip_by_eventid.py
Executable file
141
tools/model_inference/adapters/get_clip_by_eventid.py
Executable file
@@ -0,0 +1,141 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
EVENT_META_INFO_URL = "https://d.minieye.tech/ess-api/dashoard/event/meta_info"
|
||||
CLIP_ID_KEYS = ("clip_id", "clip_uuid", "clip_ukey", "ukey", "id")
|
||||
|
||||
|
||||
def fetch_event_meta(
|
||||
event_id: str,
|
||||
timeout: float = 60.0,
|
||||
max_retries: int = 3,
|
||||
retry_backoff_sec: float = 2.0,
|
||||
) -> dict[str, Any]:
|
||||
event_id = str(event_id).strip()
|
||||
if not event_id:
|
||||
raise ValueError("event_id is required")
|
||||
|
||||
last_error: Exception | None = None
|
||||
total_attempts = max(1, int(max_retries))
|
||||
for attempt in range(1, total_attempts + 1):
|
||||
try:
|
||||
response = requests.post(
|
||||
url=EVENT_META_INFO_URL,
|
||||
json={"event_id": event_id},
|
||||
timeout=timeout,
|
||||
)
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError(f"Unexpected event meta response type: {type(payload).__name__}")
|
||||
return payload
|
||||
except (requests.exceptions.Timeout, requests.exceptions.RequestException, ValueError) as exc:
|
||||
last_error = exc
|
||||
if attempt >= total_attempts:
|
||||
break
|
||||
sleep_sec = max(0.0, float(retry_backoff_sec)) * (2 ** (attempt - 1))
|
||||
print(
|
||||
f"[warn] event_id={event_id} request attempt {attempt}/{total_attempts} failed: "
|
||||
f"{type(exc).__name__}: {exc}. retry in {sleep_sec:.1f}s"
|
||||
)
|
||||
if sleep_sec > 0:
|
||||
time.sleep(sleep_sec)
|
||||
|
||||
assert last_error is not None
|
||||
raise last_error
|
||||
|
||||
|
||||
def _append_clip_id(clip_ids: list[str], seen: set[str], value: Any) -> None:
|
||||
clip_id = str(value).strip()
|
||||
if clip_id and clip_id not in seen:
|
||||
seen.add(clip_id)
|
||||
clip_ids.append(clip_id)
|
||||
|
||||
|
||||
def _extract_clip_ids_from_item(item: Any, clip_ids: list[str], seen: set[str]) -> None:
|
||||
if item is None:
|
||||
return
|
||||
if isinstance(item, str):
|
||||
_append_clip_id(clip_ids, seen, item)
|
||||
return
|
||||
if isinstance(item, (list, tuple, set)):
|
||||
for sub_item in item:
|
||||
_extract_clip_ids_from_item(sub_item, clip_ids, seen)
|
||||
return
|
||||
if isinstance(item, dict):
|
||||
matched_key = False
|
||||
for key in CLIP_ID_KEYS:
|
||||
value = item.get(key)
|
||||
if value is not None:
|
||||
matched_key = True
|
||||
_extract_clip_ids_from_item(value, clip_ids, seen)
|
||||
if matched_key:
|
||||
return
|
||||
for value in item.values():
|
||||
_extract_clip_ids_from_item(value, clip_ids, seen)
|
||||
return
|
||||
|
||||
_append_clip_id(clip_ids, seen, item)
|
||||
|
||||
|
||||
def extract_associated_clip_ids(payload: dict[str, Any]) -> list[str]:
|
||||
clip_items = payload.get("associated_clip_list", [])
|
||||
if clip_items is None:
|
||||
return []
|
||||
clip_ids: list[str] = []
|
||||
seen: set[str] = set()
|
||||
_extract_clip_ids_from_item(clip_items, clip_ids, seen)
|
||||
return clip_ids
|
||||
|
||||
|
||||
def get_associated_clip_ids(
|
||||
event_id: str,
|
||||
timeout: float = 60.0,
|
||||
max_retries: int = 3,
|
||||
retry_backoff_sec: float = 2.0,
|
||||
) -> list[str]:
|
||||
payload = fetch_event_meta(
|
||||
event_id,
|
||||
timeout=timeout,
|
||||
max_retries=max_retries,
|
||||
retry_backoff_sec=retry_backoff_sec,
|
||||
)
|
||||
return extract_associated_clip_ids(payload)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Resolve PDCL clip ids from one or more event ids.")
|
||||
parser.add_argument("event_ids", nargs="+", help="One or more event ids to resolve.")
|
||||
parser.add_argument("--timeout", type=float, default=60.0, help="HTTP request timeout in seconds.")
|
||||
parser.add_argument("--retries", type=int, default=3, help="Max request attempts per event id.")
|
||||
parser.add_argument("--retry-backoff-sec", type=float, default=2.0, help="Base retry backoff in seconds.")
|
||||
parser.add_argument("--pretty", action="store_true", help="Pretty-print the JSON result.")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
result = {
|
||||
event_id: get_associated_clip_ids(
|
||||
event_id,
|
||||
timeout=args.timeout,
|
||||
max_retries=args.retries,
|
||||
retry_backoff_sec=args.retry_backoff_sec,
|
||||
)
|
||||
for event_id in args.event_ids
|
||||
}
|
||||
if args.pretty:
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
else:
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
826
tools/model_inference/adapters/pdcl_clip_export_utils.py
Executable file
826
tools/model_inference/adapters/pdcl_clip_export_utils.py
Executable file
@@ -0,0 +1,826 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import traceback
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from threading import Lock
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
|
||||
CALIB_ATTACHMENT_NAMES = (
|
||||
"sigmastar.1/calibs/camera4.json",
|
||||
"test_data/calibs/camera4.json",
|
||||
"calibs/camera4.json",
|
||||
)
|
||||
BEIJING_TZ = timezone(timedelta(hours=8))
|
||||
DEFAULT_DATE_NAME = "unknown_date"
|
||||
DEFAULT_VEHICLE_NAME = "unknown_vehicle"
|
||||
LOCAL_MCAP_VEHICLE_NAME = "local_mcap"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ClipTask:
|
||||
clip_uuid: str
|
||||
date_name: str
|
||||
vehicle_name: str
|
||||
clip_path: str
|
||||
|
||||
@property
|
||||
def task_id(self) -> str:
|
||||
return self.clip_uuid
|
||||
|
||||
|
||||
@dataclass
|
||||
class TaskResult:
|
||||
task_id: str
|
||||
success: bool
|
||||
message: str
|
||||
output_dir: Optional[str] = None
|
||||
|
||||
|
||||
class StatusStore:
|
||||
"""Persistent status tracker for resumable batch inference."""
|
||||
|
||||
def __init__(self, status_file: Path):
|
||||
self.status_file = status_file
|
||||
self.lock = Lock()
|
||||
self._status: dict[str, dict[str, str]] = {}
|
||||
self.status_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
if self.status_file.exists():
|
||||
try:
|
||||
self._status = json.loads(self.status_file.read_text())
|
||||
except Exception:
|
||||
self._status = {}
|
||||
|
||||
def is_done(self, task_id: str) -> bool:
|
||||
return self._status.get(task_id, {}).get("state") == "done"
|
||||
|
||||
def get(self, task_id: str) -> Optional[dict[str, str]]:
|
||||
return self._status.get(task_id)
|
||||
|
||||
def mark_running(self, task_id: str, detail: str) -> None:
|
||||
with self.lock:
|
||||
self._status[task_id] = {"state": "running", "detail": detail}
|
||||
self._flush()
|
||||
|
||||
def mark_done(self, task_id: str, detail: str) -> None:
|
||||
with self.lock:
|
||||
self._status[task_id] = {"state": "done", "detail": detail}
|
||||
self._flush()
|
||||
|
||||
def mark_failed(self, task_id: str, detail: str) -> None:
|
||||
with self.lock:
|
||||
self._status[task_id] = {"state": "failed", "detail": detail}
|
||||
self._flush()
|
||||
|
||||
def summary(self) -> dict[str, int]:
|
||||
done = 0
|
||||
running = 0
|
||||
failed = 0
|
||||
for item in self._status.values():
|
||||
state = item.get("state")
|
||||
if state == "done":
|
||||
done += 1
|
||||
elif state == "running":
|
||||
running += 1
|
||||
elif state == "failed":
|
||||
failed += 1
|
||||
return {"done": done, "running": running, "failed": failed, "total": len(self._status)}
|
||||
|
||||
def _flush(self) -> None:
|
||||
self.status_file.write_text(json.dumps(self._status, indent=2, ensure_ascii=False))
|
||||
|
||||
|
||||
def _ensure_pdcl_auth_defaults() -> None:
|
||||
os.environ.setdefault("STS_UID", "dis-uploader")
|
||||
os.environ.setdefault("STS_SECRET_KEY", "277310cc09724d315514a79701fecb0f")
|
||||
|
||||
|
||||
def validate_pdcl_auth_env() -> None:
|
||||
_ensure_pdcl_auth_defaults()
|
||||
required_vars = ["STS_UID", "STS_SECRET_KEY"]
|
||||
missing = [name for name in required_vars if not os.getenv(name)]
|
||||
if missing:
|
||||
raise ValueError(
|
||||
"Missing required PDCL auth env vars: "
|
||||
+ ", ".join(missing)
|
||||
+ ". Please export them in shell or set them in .env before running."
|
||||
)
|
||||
|
||||
|
||||
def _normalize_metadata_value(value: Any) -> str:
|
||||
text = str(value or "").strip()
|
||||
return text if text and text.lower() != "none" else ""
|
||||
|
||||
|
||||
def _normalize_frame_name_token(value: Any) -> str:
|
||||
text = str(value or "").strip()
|
||||
if not text or text.lower() == "none":
|
||||
return ""
|
||||
return text.replace("/", "_").replace("\\", "_").replace(" ", "")
|
||||
|
||||
|
||||
def _format_ms_to_date_name(timestamp_ms: Any) -> str:
|
||||
if not isinstance(timestamp_ms, (int, float)) or timestamp_ms <= 0:
|
||||
return ""
|
||||
dt = datetime.fromtimestamp(timestamp_ms / 1000, tz=timezone.utc).astimezone(BEIJING_TZ)
|
||||
return dt.strftime("%Y%m%d%H%M%S")
|
||||
|
||||
|
||||
def _build_clip_task(clip_uuid: str) -> Optional[ClipTask]:
|
||||
_ensure_pdcl_auth_defaults()
|
||||
from pdcl_dss import Clip
|
||||
|
||||
date_name = DEFAULT_DATE_NAME
|
||||
vehicle_name = DEFAULT_VEHICLE_NAME
|
||||
|
||||
try:
|
||||
clip = Clip(clip_uuid)
|
||||
clip_meta = dict(clip.meta)
|
||||
files, _ = clip.list_files()
|
||||
if not files:
|
||||
print(f"Skip clip_id={clip_uuid}: no files found")
|
||||
return None
|
||||
clip_path = clip.get_cache_path(files[0])
|
||||
except Exception as exc:
|
||||
print(f"Skip clip_id={clip_uuid}: failed to load clip metadata. {type(exc).__name__}: {exc}")
|
||||
return None
|
||||
|
||||
clip_date_name = _format_ms_to_date_name(clip_meta.get("start_time_ms"))
|
||||
if clip_date_name:
|
||||
date_name = clip_date_name
|
||||
|
||||
try:
|
||||
group_id = clip.get_group_id()
|
||||
if group_id:
|
||||
group_meta = dict(clip.get_group().meta)
|
||||
for key in ("plate_number", "vehicle_name", "car_name", "plateNumber", "vehicleName", "carName"):
|
||||
value = _normalize_metadata_value(group_meta.get(key))
|
||||
if value:
|
||||
vehicle_name = value
|
||||
break
|
||||
|
||||
project_name = _normalize_metadata_value(group_meta.get("project_name"))
|
||||
if vehicle_name == DEFAULT_VEHICLE_NAME and project_name:
|
||||
vehicle_name = project_name
|
||||
|
||||
group_date_name = _format_ms_to_date_name(group_meta.get("collection_time"))
|
||||
if group_date_name:
|
||||
date_name = group_date_name
|
||||
except Exception as exc:
|
||||
print(
|
||||
f"[warn] clip_id={clip_uuid}: failed to load group metadata, "
|
||||
f"fallback vehicle={vehicle_name} date={date_name}. {type(exc).__name__}: {exc}"
|
||||
)
|
||||
|
||||
return ClipTask(
|
||||
clip_uuid=clip_uuid,
|
||||
date_name=date_name,
|
||||
vehicle_name=vehicle_name,
|
||||
clip_path=clip_path,
|
||||
)
|
||||
|
||||
|
||||
def parse_clip_list(clip_list_file: str) -> list[ClipTask]:
|
||||
tasks: list[ClipTask] = []
|
||||
with open(clip_list_file, "r", encoding="utf-8") as file:
|
||||
for line in file:
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
|
||||
clip_uuid = line.split()[0]
|
||||
task = _build_clip_task(clip_uuid)
|
||||
if task is not None:
|
||||
tasks.append(task)
|
||||
return tasks
|
||||
|
||||
|
||||
def build_clip_tasks_from_clip_ids(clip_ids: list[str]) -> list[ClipTask]:
|
||||
tasks: list[ClipTask] = []
|
||||
seen: set[str] = set()
|
||||
for clip_id in clip_ids:
|
||||
clip_uuid = str(clip_id).strip()
|
||||
if not clip_uuid or clip_uuid in seen:
|
||||
continue
|
||||
seen.add(clip_uuid)
|
||||
task = _build_clip_task(clip_uuid)
|
||||
if task is not None:
|
||||
tasks.append(task)
|
||||
return tasks
|
||||
|
||||
|
||||
def build_clip_task_from_mcap_file(
|
||||
mcap_file: str | Path,
|
||||
*,
|
||||
clip_id: str = "",
|
||||
date_name: str = "",
|
||||
vehicle_name: str = "",
|
||||
) -> ClipTask:
|
||||
mcap_path = Path(mcap_file).expanduser().resolve()
|
||||
if not mcap_path.is_file():
|
||||
raise FileNotFoundError(f"MCAP file not found: {mcap_path}")
|
||||
|
||||
stem = mcap_path.stem
|
||||
resolved_clip_id = _normalize_metadata_value(clip_id) or stem
|
||||
resolved_date_name = _normalize_metadata_value(date_name)
|
||||
if not resolved_date_name and len(stem) == 14 and stem.isdigit():
|
||||
resolved_date_name = stem
|
||||
resolved_vehicle_name = _normalize_metadata_value(vehicle_name) or LOCAL_MCAP_VEHICLE_NAME
|
||||
|
||||
return ClipTask(
|
||||
clip_uuid=resolved_clip_id,
|
||||
date_name=resolved_date_name or DEFAULT_DATE_NAME,
|
||||
vehicle_name=resolved_vehicle_name,
|
||||
clip_path=str(mcap_path),
|
||||
)
|
||||
|
||||
|
||||
def build_case_dir_name(prefix: str, clip_task: ClipTask) -> str:
|
||||
safe_clip_uuid = clip_task.clip_uuid.replace("/", "_")
|
||||
return f"{prefix}_{clip_task.vehicle_name}_{clip_task.date_name}_{safe_clip_uuid}"
|
||||
|
||||
|
||||
def _plane_to_ndarray(plane: Any) -> np.ndarray:
|
||||
stride = plane.line_size
|
||||
height = plane.height
|
||||
width = plane.width
|
||||
array = np.frombuffer(plane, dtype=np.uint8)
|
||||
if stride == width:
|
||||
return array.reshape(height, width)
|
||||
return array.reshape(height, stride)[:, :width]
|
||||
|
||||
|
||||
def _yuvj420p_to_nv12(
|
||||
y_plane: np.ndarray,
|
||||
u_plane: np.ndarray,
|
||||
v_plane: np.ndarray,
|
||||
) -> tuple[np.ndarray, np.ndarray]:
|
||||
uv_height = u_plane.shape[0]
|
||||
uv_width = u_plane.shape[1]
|
||||
uv_plane = np.zeros((uv_height, uv_width * 2), dtype=np.uint8)
|
||||
uv_plane[:, 0::2] = u_plane
|
||||
uv_plane[:, 1::2] = v_plane
|
||||
return y_plane.copy(), uv_plane
|
||||
|
||||
|
||||
def _h265_payload_to_bgr(payload: bytes) -> np.ndarray:
|
||||
try:
|
||||
import av
|
||||
except ImportError as exc:
|
||||
raise ImportError("PyAV is required for mcap decoding. Please install: pip install av") from exc
|
||||
|
||||
container = av.open(io.BytesIO(payload))
|
||||
for frame in container.decode(video=0):
|
||||
y_plane = _plane_to_ndarray(frame.planes[0])
|
||||
u_plane = _plane_to_ndarray(frame.planes[1])
|
||||
v_plane = _plane_to_ndarray(frame.planes[2])
|
||||
y_nv12, uv_nv12 = _yuvj420p_to_nv12(y_plane, u_plane, v_plane)
|
||||
yuv_image = np.concatenate((y_nv12, uv_nv12), axis=0)
|
||||
return cv2.cvtColor(yuv_image, cv2.COLOR_YUV2BGR_NV12)
|
||||
raise ValueError("decode failed: no video frame in payload")
|
||||
|
||||
|
||||
def _resolve_calib_file_for_clip(clip_path: str) -> Optional[Path]:
|
||||
base = Path(clip_path).resolve()
|
||||
search_dir = base.parent
|
||||
candidates: list[Path] = []
|
||||
|
||||
for _ in range(4):
|
||||
candidates.extend(
|
||||
[
|
||||
search_dir / "calibs" / "camera4.json",
|
||||
search_dir / "test_data" / "calibs" / "camera4.json",
|
||||
search_dir / "L2_calib" / "camera4.json",
|
||||
search_dir / "calib" / "L2_calib" / "camera4.json",
|
||||
]
|
||||
)
|
||||
parent = search_dir.parent
|
||||
if parent == search_dir:
|
||||
break
|
||||
search_dir = parent
|
||||
|
||||
for calib_path in candidates:
|
||||
if calib_path.exists():
|
||||
return calib_path
|
||||
return None
|
||||
|
||||
|
||||
def _candidate_camera_topics(camera_topic: str) -> list[str]:
|
||||
topic = str(camera_topic or "").strip()
|
||||
if not topic:
|
||||
return []
|
||||
|
||||
topics = [topic]
|
||||
if "." in topic:
|
||||
topics.append(topic.rsplit(".", 1)[-1])
|
||||
else:
|
||||
topics.append(f"sigmastar.1.{topic}")
|
||||
return list(dict.fromkeys(topics))
|
||||
|
||||
|
||||
def _resolve_reader_camera_topics(reader: Any, camera_topic: str) -> list[str]:
|
||||
candidates = _candidate_camera_topics(camera_topic)
|
||||
topic_map = getattr(reader, "_topic_map", None)
|
||||
if isinstance(topic_map, dict):
|
||||
for candidate in candidates:
|
||||
if candidate in topic_map:
|
||||
return [candidate]
|
||||
for key, values in topic_map.items():
|
||||
if any(candidate in values for candidate in candidates):
|
||||
return [key]
|
||||
return candidates
|
||||
|
||||
|
||||
def _decode_mcap_to_images(
|
||||
clip_uuid: str,
|
||||
clip_path: str,
|
||||
output_dir: Path,
|
||||
camera_topic: str,
|
||||
max_frames: int,
|
||||
) -> tuple[int, Optional[dict[str, Any]], list[dict[str, Any]]]:
|
||||
from pdcl_pyclip.decoder_struct import StructDecoder
|
||||
from pdcl_pyclip.msg_camera import VideoMessage
|
||||
from pdcl_pyclip.reader import ClipReader
|
||||
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
reader = ClipReader(clip_path)
|
||||
struct_decoder = StructDecoder()
|
||||
saved = 0
|
||||
camera4_json = None
|
||||
decode_errors: list[dict[str, Any]] = []
|
||||
camera_topics = _resolve_reader_camera_topics(reader, camera_topic)
|
||||
|
||||
for schema, channel, msg in reader.iter_messages(topics=camera_topics):
|
||||
if schema.encoding != "struct":
|
||||
continue
|
||||
data = struct_decoder.decode(schema, channel, msg)
|
||||
if not isinstance(data, VideoMessage):
|
||||
continue
|
||||
|
||||
try:
|
||||
image = _h265_payload_to_bgr(data.payload)
|
||||
except Exception as exc:
|
||||
frame_id = getattr(data, "frame_id", None)
|
||||
error_record = {
|
||||
"frame_id": None if frame_id is None else str(frame_id),
|
||||
"error_type": type(exc).__name__,
|
||||
"error": str(exc),
|
||||
}
|
||||
decode_errors.append(error_record)
|
||||
print(
|
||||
f"[warn] clip_id={clip_uuid} frame_id={error_record['frame_id']} "
|
||||
f"-> skip bad frame: {error_record['error_type']}: {error_record['error']}"
|
||||
)
|
||||
continue
|
||||
|
||||
frame_id_token = _normalize_frame_name_token(getattr(data, "frame_id", None))
|
||||
timestamp_token = _normalize_frame_name_token(getattr(msg, "log_time", None))
|
||||
if frame_id_token and timestamp_token:
|
||||
frame_name = f"{clip_uuid}_{frame_id_token}_{timestamp_token}.png"
|
||||
elif frame_id_token:
|
||||
frame_name = f"{clip_uuid}_{frame_id_token}.png"
|
||||
elif timestamp_token:
|
||||
frame_name = f"{clip_uuid}_{saved:06d}_{timestamp_token}.png"
|
||||
else:
|
||||
frame_name = f"{clip_uuid}_{saved:06d}.png"
|
||||
if not cv2.imwrite(str(output_dir / frame_name), image):
|
||||
raise IOError(f"Failed to write image: {output_dir / frame_name}")
|
||||
|
||||
saved += 1
|
||||
if max_frames > 0 and saved >= max_frames:
|
||||
break
|
||||
|
||||
for attachment in reader.iter_attachments():
|
||||
if attachment.name in CALIB_ATTACHMENT_NAMES:
|
||||
camera4_json = json.loads(attachment.data.decode("utf-8"))
|
||||
break
|
||||
|
||||
return saved, camera4_json, decode_errors
|
||||
|
||||
|
||||
def _save_decode_errors(output_dir: Path, clip_uuid: str, decode_errors: list[dict[str, Any]]) -> Optional[Path]:
|
||||
if not decode_errors:
|
||||
return None
|
||||
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
decode_errors_path = output_dir / "decode_errors.json"
|
||||
payload = {
|
||||
"clip_uuid": clip_uuid,
|
||||
"skipped_bad_frame_count": len(decode_errors),
|
||||
"errors": decode_errors,
|
||||
}
|
||||
with open(decode_errors_path, "w", encoding="utf-8") as file:
|
||||
json.dump(payload, file, indent=2, ensure_ascii=False)
|
||||
return decode_errors_path
|
||||
|
||||
|
||||
def _save_calib_for_clip(
|
||||
clip_path: str,
|
||||
calib_output_path: Path,
|
||||
calib_file_override: str,
|
||||
embedded_calib: Optional[dict[str, Any]],
|
||||
) -> str:
|
||||
calib_output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if calib_file_override:
|
||||
calib_src = Path(calib_file_override).resolve()
|
||||
if not calib_src.exists():
|
||||
raise FileNotFoundError(f"Calibration file {calib_src} does not exist.")
|
||||
calib_output_path.write_bytes(calib_src.read_bytes())
|
||||
return f"override:{calib_src}"
|
||||
|
||||
calib_src = _resolve_calib_file_for_clip(clip_path)
|
||||
if calib_src is not None:
|
||||
calib_output_path.write_bytes(calib_src.read_bytes())
|
||||
return f"file:{calib_src}"
|
||||
|
||||
if embedded_calib is None:
|
||||
raise FileNotFoundError(
|
||||
"Calibration file camera4.json not found near clip and not found in mcap attachments. "
|
||||
"Please provide --calib-file explicitly."
|
||||
)
|
||||
|
||||
with open(calib_output_path, "w", encoding="utf-8") as file:
|
||||
json.dump(embedded_calib, file, indent=2, ensure_ascii=False)
|
||||
return "mcap_attachment"
|
||||
|
||||
|
||||
def _build_calib_summary(calib_payload: Optional[dict[str, Any]]) -> dict[str, Any]:
|
||||
if not isinstance(calib_payload, dict):
|
||||
return {
|
||||
"format": "unknown",
|
||||
"has_distortion": False,
|
||||
}
|
||||
|
||||
intrinsics = calib_payload
|
||||
extrinsics = None
|
||||
calib_format = "flat_camera4"
|
||||
|
||||
if "intrinsics" in calib_payload:
|
||||
intrinsics = calib_payload.get("intrinsics", {}).get("camera4.json", {}) or {}
|
||||
extrinsics = calib_payload.get("extrinsics", {}).get("camera4.json", {}) or {}
|
||||
calib_format = "combined_calibration"
|
||||
|
||||
distort_coeffs = intrinsics.get("distort_coeffs", calib_payload.get("distort_coeffs", [])) or []
|
||||
return {
|
||||
"format": calib_format,
|
||||
"focal_u": intrinsics.get("focal_u"),
|
||||
"focal_v": intrinsics.get("focal_v"),
|
||||
"cu": intrinsics.get("cu"),
|
||||
"cv": intrinsics.get("cv"),
|
||||
"pitch": calib_payload.get("pitch", extrinsics.get("rpy", [None, None, None])[1] if extrinsics else None),
|
||||
"yaw": calib_payload.get("yaw", extrinsics.get("rpy", [None, None, None])[2] if extrinsics else None),
|
||||
"image_width": calib_payload.get("image_width", calib_payload.get("img_width")),
|
||||
"image_height": calib_payload.get("image_height", calib_payload.get("img_height")),
|
||||
"distort_coeff_count": len(distort_coeffs),
|
||||
"has_distortion": bool(distort_coeffs),
|
||||
}
|
||||
|
||||
|
||||
def _save_export_manifest(
|
||||
output_dir: Path,
|
||||
clip_task: ClipTask,
|
||||
frame_count: int,
|
||||
calib_source: str,
|
||||
camera_topic: str,
|
||||
calib_summary: Optional[dict[str, Any]] = None,
|
||||
skipped_bad_frame_count: int = 0,
|
||||
decode_errors_path: Optional[str] = None,
|
||||
) -> None:
|
||||
manifest = {
|
||||
"clip_uuid": clip_task.clip_uuid,
|
||||
"date_name": clip_task.date_name,
|
||||
"vehicle_name": clip_task.vehicle_name,
|
||||
"clip_path": clip_task.clip_path,
|
||||
"camera_topic": camera_topic,
|
||||
"frame_count": frame_count,
|
||||
"skipped_bad_frame_count": skipped_bad_frame_count,
|
||||
"calib_source": calib_source,
|
||||
"calib_summary": calib_summary,
|
||||
}
|
||||
if decode_errors_path:
|
||||
manifest["decode_errors_path"] = decode_errors_path
|
||||
with open(output_dir / "manifest.json", "w", encoding="utf-8") as file:
|
||||
json.dump(manifest, file, indent=2, ensure_ascii=False)
|
||||
|
||||
|
||||
def _input_source_label(args: argparse.Namespace) -> str:
|
||||
return str(getattr(args, "mcap_file", "") or getattr(args, "clip_list_file", ""))
|
||||
|
||||
|
||||
def _input_source_mode(args: argparse.Namespace) -> str:
|
||||
if getattr(args, "mcap_file", ""):
|
||||
return "mcap_file"
|
||||
return "clip_list"
|
||||
|
||||
|
||||
def _save_calib_summary(output_dir: Path, calib_source: str, calib_summary: dict[str, Any]) -> None:
|
||||
payload = {
|
||||
"calib_source": calib_source,
|
||||
"calib_summary": calib_summary,
|
||||
}
|
||||
with open(output_dir / "calib_summary.json", "w", encoding="utf-8") as file:
|
||||
json.dump(payload, file, indent=2, ensure_ascii=False)
|
||||
|
||||
|
||||
def export_one_clip(
|
||||
clip_task: ClipTask,
|
||||
args: argparse.Namespace,
|
||||
status: StatusStore,
|
||||
) -> TaskResult:
|
||||
task_id = clip_task.task_id
|
||||
if args.skip_done and status.is_done(task_id):
|
||||
info = status.get(task_id) or {}
|
||||
return TaskResult(task_id=task_id, success=True, message=f"skip done: {info.get('detail', '')}")
|
||||
|
||||
case_dir = Path(args.output_root) / build_case_dir_name(args.output_prefix, clip_task)
|
||||
images_dir = case_dir / "images"
|
||||
calib_path = case_dir / "calib" / "L2_calib" / "camera4.json"
|
||||
status.mark_running(task_id, f"running source={clip_task.clip_path}")
|
||||
|
||||
try:
|
||||
frame_count, embedded_calib, decode_errors = _decode_mcap_to_images(
|
||||
clip_uuid=clip_task.clip_uuid,
|
||||
clip_path=clip_task.clip_path,
|
||||
output_dir=images_dir,
|
||||
camera_topic=args.camera_topic,
|
||||
max_frames=args.max_frames_per_clip,
|
||||
)
|
||||
decode_errors_path = _save_decode_errors(case_dir, clip_task.clip_uuid, decode_errors)
|
||||
if frame_count <= 0:
|
||||
if decode_errors:
|
||||
first_error = decode_errors[0]
|
||||
raise RuntimeError(
|
||||
f"Failed to decode any frames; skipped {len(decode_errors)} bad frames. "
|
||||
f"First error: {first_error['error_type']}: {first_error['error']}. "
|
||||
f"See {decode_errors_path}"
|
||||
)
|
||||
raise RuntimeError(
|
||||
f"No frames were decoded from clip {clip_task.clip_uuid} on topic "
|
||||
f"{args.camera_topic!r} (tried: {', '.join(_candidate_camera_topics(args.camera_topic))})."
|
||||
)
|
||||
calib_source = _save_calib_for_clip(
|
||||
clip_path=clip_task.clip_path,
|
||||
calib_output_path=calib_path,
|
||||
calib_file_override=args.calib_file,
|
||||
embedded_calib=embedded_calib,
|
||||
)
|
||||
with open(calib_path, "r", encoding="utf-8") as file:
|
||||
calib_payload = json.load(file)
|
||||
calib_summary = _build_calib_summary(calib_payload)
|
||||
_save_export_manifest(
|
||||
output_dir=case_dir,
|
||||
clip_task=clip_task,
|
||||
frame_count=frame_count,
|
||||
calib_source=calib_source,
|
||||
camera_topic=args.camera_topic,
|
||||
calib_summary=calib_summary,
|
||||
skipped_bad_frame_count=len(decode_errors),
|
||||
decode_errors_path=str(decode_errors_path) if decode_errors_path else None,
|
||||
)
|
||||
_save_calib_summary(case_dir, calib_source=calib_source, calib_summary=calib_summary)
|
||||
status.mark_done(task_id, str(case_dir))
|
||||
return TaskResult(
|
||||
task_id=task_id,
|
||||
success=True,
|
||||
message=f"ok frames={frame_count} skipped_bad_frames={len(decode_errors)}",
|
||||
output_dir=str(case_dir),
|
||||
)
|
||||
except Exception as exc:
|
||||
message = f"{type(exc).__name__}: {exc}"
|
||||
status.mark_failed(task_id, message)
|
||||
err_log = Path(args.output_root) / "_status" / "errors.log"
|
||||
err_log.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(err_log, "a", encoding="utf-8") as file:
|
||||
file.write(f"[{task_id}] {message}\n{traceback.format_exc()}\n")
|
||||
return TaskResult(task_id=task_id, success=False, message=message)
|
||||
|
||||
|
||||
def save_run_manifest(args: argparse.Namespace, clip_tasks: list[ClipTask]) -> None:
|
||||
manifest_path = Path(args.output_root) / "_status" / "run_manifest.json"
|
||||
manifest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
payload = {
|
||||
"source_mode": _input_source_mode(args),
|
||||
"source": _input_source_label(args),
|
||||
"clip_list_file": getattr(args, "clip_list_file", ""),
|
||||
"mcap_file": getattr(args, "mcap_file", ""),
|
||||
"output_root": args.output_root,
|
||||
"camera_topic": args.camera_topic,
|
||||
"max_frames_per_clip": args.max_frames_per_clip,
|
||||
"num_tasks": len(clip_tasks),
|
||||
"tasks": [
|
||||
{
|
||||
"task_id": task.task_id,
|
||||
"clip_uuid": task.clip_uuid,
|
||||
"date_name": task.date_name,
|
||||
"vehicle_name": task.vehicle_name,
|
||||
"clip_path": task.clip_path,
|
||||
}
|
||||
for task in clip_tasks
|
||||
],
|
||||
}
|
||||
with open(manifest_path, "w", encoding="utf-8") as file:
|
||||
json.dump(payload, file, indent=2, ensure_ascii=False)
|
||||
|
||||
|
||||
def has_reusable_exported_case(case_dir: Path) -> bool:
|
||||
images_dir = case_dir / "images"
|
||||
calib_path = case_dir / "calib" / "L2_calib" / "camera4.json"
|
||||
if not images_dir.is_dir() or not calib_path.is_file():
|
||||
return False
|
||||
return any(path.is_file() for path in images_dir.iterdir())
|
||||
|
||||
|
||||
def save_exported_batch_manifest(args: argparse.Namespace, clip_tasks: list[ClipTask], output_root: Path) -> None:
|
||||
manifest_path = output_root / "_status" / "run_manifest.json"
|
||||
manifest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
payload = {
|
||||
"source_mode": _input_source_mode(args),
|
||||
"source": _input_source_label(args),
|
||||
"clip_list_file": getattr(args, "clip_list_file", ""),
|
||||
"mcap_file": getattr(args, "mcap_file", ""),
|
||||
"export_root": args.export_root,
|
||||
"output_dir": str(output_root),
|
||||
"camera_topic": args.camera_topic,
|
||||
"max_frames_per_clip": args.max_frames_per_clip,
|
||||
"exported_model": args.exported_model,
|
||||
"edge_yaw_max_lateral_dist_m": args.edge_yaw_max_lateral_dist,
|
||||
"roi_models": {
|
||||
"roi0": args.roi0_model,
|
||||
"roi1": args.roi1_model,
|
||||
},
|
||||
"num_tasks": len(clip_tasks),
|
||||
"tasks": [
|
||||
{
|
||||
"task_id": task.task_id,
|
||||
"clip_uuid": task.clip_uuid,
|
||||
"date_name": task.date_name,
|
||||
"vehicle_name": task.vehicle_name,
|
||||
"clip_path": task.clip_path,
|
||||
}
|
||||
for task in clip_tasks
|
||||
],
|
||||
}
|
||||
with manifest_path.open("w", encoding="utf-8") as file:
|
||||
json.dump(payload, file, indent=2, ensure_ascii=False)
|
||||
|
||||
|
||||
def run_clip_tasks_inference_exported(
|
||||
*,
|
||||
context: Any,
|
||||
args: argparse.Namespace,
|
||||
clip_tasks: list[ClipTask],
|
||||
load_env: Callable[[], Any],
|
||||
run_case_inference: Callable[..., dict[str, Any]],
|
||||
export_root: str | Path | None = None,
|
||||
output_root: str | Path | None = None,
|
||||
on_before_run: Optional[Callable[[Path, Path, list[ClipTask]], None]] = None,
|
||||
require_pdcl_auth: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
load_env()
|
||||
if require_pdcl_auth:
|
||||
validate_pdcl_auth_env()
|
||||
|
||||
export_root = Path(args.export_root if export_root is None else export_root).resolve()
|
||||
output_root = Path(args.output_dir if output_root is None else output_root).resolve()
|
||||
export_root.mkdir(parents=True, exist_ok=True)
|
||||
output_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
args.output_root = str(export_root)
|
||||
|
||||
if not clip_tasks:
|
||||
print("No clip tasks discovered. Exit.")
|
||||
return {
|
||||
"source_mode": _input_source_mode(args),
|
||||
"source": _input_source_label(args),
|
||||
"clip_list_file": getattr(args, "clip_list_file", ""),
|
||||
"mcap_file": getattr(args, "mcap_file", ""),
|
||||
"output_dir": str(output_root),
|
||||
"total": 0,
|
||||
"success": 0,
|
||||
"failed": 0,
|
||||
}
|
||||
|
||||
export_status = StatusStore(export_root / "_status" / "task_status.json")
|
||||
infer_status = StatusStore(output_root / "_status" / "task_status.json")
|
||||
if on_before_run is not None:
|
||||
on_before_run(export_root, output_root, clip_tasks)
|
||||
save_run_manifest(args, clip_tasks)
|
||||
save_exported_batch_manifest(args, clip_tasks, output_root)
|
||||
|
||||
print(f"Discovered {len(clip_tasks)} clips.")
|
||||
print(f"Export root: {export_root}")
|
||||
print(f"Visualization root: {output_root}")
|
||||
|
||||
success = 0
|
||||
fail = 0
|
||||
for index, clip_task in enumerate(clip_tasks, start=1):
|
||||
task_id = clip_task.task_id
|
||||
if args.skip_done and infer_status.is_done(task_id):
|
||||
info = infer_status.get(task_id) or {}
|
||||
print(f"[{index}/{len(clip_tasks)}] clip_id={task_id} -> skip done: {info.get('detail', '')}")
|
||||
continue
|
||||
|
||||
print(f"[{index}/{len(clip_tasks)}] clip_id={task_id} source={clip_task.clip_path}")
|
||||
infer_status.mark_running(task_id, "exporting clip and running exported two-roi inference")
|
||||
try:
|
||||
case_dir = export_root / build_case_dir_name(args.output_prefix, clip_task)
|
||||
if has_reusable_exported_case(case_dir):
|
||||
export_status.mark_done(task_id, f"reuse existing export: {case_dir}")
|
||||
print(f" -> reuse exported clip data from {case_dir}")
|
||||
else:
|
||||
export_result = export_one_clip(clip_task, args, export_status)
|
||||
if not export_result.success or not export_result.output_dir:
|
||||
raise RuntimeError(export_result.message)
|
||||
case_dir = Path(export_result.output_dir)
|
||||
|
||||
infer_result = run_case_inference(
|
||||
context=context,
|
||||
case_dir=str(case_dir),
|
||||
output_dir=output_root / case_dir.name,
|
||||
glob_pattern=args.glob,
|
||||
max_images=args.max_images,
|
||||
save_aggregate_predictions=bool(getattr(args, "save_aggregate_predictions", False)),
|
||||
save_visualizations=not bool(getattr(args, "skip_visualizations", False)),
|
||||
)
|
||||
infer_status.mark_done(task_id, infer_result["output_dir"])
|
||||
success += 1
|
||||
print(f" -> saved {infer_result['num_frames']} frames to {infer_result['output_dir']}")
|
||||
except Exception as exc:
|
||||
fail += 1
|
||||
message = f"{type(exc).__name__}: {exc}"
|
||||
infer_status.mark_failed(task_id, message)
|
||||
err_log = output_root / "_status" / "errors.log"
|
||||
err_log.parent.mkdir(parents=True, exist_ok=True)
|
||||
with err_log.open("a", encoding="utf-8") as file:
|
||||
file.write(f"[{task_id}] {message}\n{traceback.format_exc()}\n")
|
||||
print(f" -> failed: {message}")
|
||||
|
||||
print("\nBatch inference done.")
|
||||
print(f"success={success}, fail={fail}")
|
||||
print(f"infer_status_summary={infer_status.summary()}")
|
||||
return {
|
||||
"source_mode": _input_source_mode(args),
|
||||
"source": _input_source_label(args),
|
||||
"clip_list_file": getattr(args, "clip_list_file", ""),
|
||||
"mcap_file": getattr(args, "mcap_file", ""),
|
||||
"export_root": str(export_root),
|
||||
"output_dir": str(output_root),
|
||||
"total": len(clip_tasks),
|
||||
"success": success,
|
||||
"failed": fail,
|
||||
}
|
||||
|
||||
|
||||
def run_clip_list_inference_exported(
|
||||
*,
|
||||
context: Any,
|
||||
args: argparse.Namespace,
|
||||
load_env: Callable[[], Any],
|
||||
run_case_inference: Callable[..., dict[str, Any]],
|
||||
) -> dict[str, Any]:
|
||||
clip_tasks = parse_clip_list(args.clip_list_file)
|
||||
if args.limit_clips > 0:
|
||||
clip_tasks = clip_tasks[: args.limit_clips]
|
||||
return run_clip_tasks_inference_exported(
|
||||
context=context,
|
||||
args=args,
|
||||
clip_tasks=clip_tasks,
|
||||
load_env=load_env,
|
||||
run_case_inference=run_case_inference,
|
||||
)
|
||||
|
||||
|
||||
def run_mcap_file_inference_exported(
|
||||
*,
|
||||
context: Any,
|
||||
args: argparse.Namespace,
|
||||
load_env: Callable[[], Any],
|
||||
run_case_inference: Callable[..., dict[str, Any]],
|
||||
) -> dict[str, Any]:
|
||||
clip_task = build_clip_task_from_mcap_file(
|
||||
args.mcap_file,
|
||||
clip_id=args.mcap_clip_id,
|
||||
date_name=args.mcap_date_name,
|
||||
vehicle_name=args.mcap_vehicle_name,
|
||||
)
|
||||
result = run_clip_tasks_inference_exported(
|
||||
context=context,
|
||||
args=args,
|
||||
clip_tasks=[clip_task],
|
||||
load_env=load_env,
|
||||
run_case_inference=run_case_inference,
|
||||
require_pdcl_auth=False,
|
||||
)
|
||||
if result.get("failed", 0) > 0 or result.get("success", 0) < result.get("total", 0):
|
||||
raise RuntimeError(f"MCAP inference failed for {args.mcap_file}")
|
||||
return result
|
||||
247
tools/model_inference/adapters/video_dir_inference_utils.py
Executable file
247
tools/model_inference/adapters/video_dir_inference_utils.py
Executable file
@@ -0,0 +1,247 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable, Optional
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
|
||||
DEFAULT_CNCAP_PATH_PREFIX_SRC = "/mnt/hfs/project-G1M3"
|
||||
DEFAULT_CNCAP_PATH_PREFIX_DST = "/mnt/G1M3"
|
||||
DEFAULT_OUTPUT_RELATIVE_ANCHORS = ("CNCAP2024数采", "gt_org_data")
|
||||
|
||||
|
||||
def rewrite_path_prefix(path_str: str, prefix_src: str, prefix_dst: str) -> str:
|
||||
normalized_path = str(path_str).strip()
|
||||
normalized_src = str(prefix_src).rstrip("/")
|
||||
normalized_dst = str(prefix_dst).rstrip("/")
|
||||
if normalized_src and normalized_path.startswith(normalized_src):
|
||||
return f"{normalized_dst}{normalized_path[len(normalized_src):]}"
|
||||
return normalized_path
|
||||
|
||||
|
||||
def load_path_list_from_json(json_file: str | Path, values_key: str = "values") -> list[str]:
|
||||
json_path = Path(json_file).resolve()
|
||||
with json_path.open("r", encoding="utf-8") as file:
|
||||
payload = json.load(file)
|
||||
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError(f"Expected top-level dict in {json_path}, got {type(payload).__name__}")
|
||||
|
||||
values = payload.get(values_key)
|
||||
if not isinstance(values, list):
|
||||
raise ValueError(f"Expected {values_key!r} list in {json_path}, got {type(values).__name__}")
|
||||
|
||||
return [str(item).strip() for item in values if str(item).strip()]
|
||||
|
||||
|
||||
def _normalize_video_case_input(video_case_dir: str | Path) -> Path:
|
||||
input_path = Path(video_case_dir).resolve()
|
||||
if input_path.is_file() and input_path.name == "camera4.bin":
|
||||
return input_path.parent.parent
|
||||
if input_path.is_dir() and input_path.name == "sigmastar.1":
|
||||
return input_path.parent
|
||||
return input_path
|
||||
|
||||
|
||||
def build_case_output_rel_dir(
|
||||
case_dir: str | Path,
|
||||
preferred_anchor_names: Iterable[str] = DEFAULT_OUTPUT_RELATIVE_ANCHORS,
|
||||
) -> Path:
|
||||
resolved_case_dir = Path(case_dir).resolve()
|
||||
parts = resolved_case_dir.parts
|
||||
|
||||
for anchor_name in preferred_anchor_names:
|
||||
if anchor_name in parts:
|
||||
anchor_index = parts.index(anchor_name)
|
||||
suffix_parts = parts[anchor_index + 1 :]
|
||||
if suffix_parts:
|
||||
return Path(*suffix_parts)
|
||||
|
||||
if len(parts) >= 3:
|
||||
return Path(*parts[-3:])
|
||||
if len(parts) >= 2:
|
||||
return Path(*parts[-2:])
|
||||
if parts:
|
||||
return Path(parts[-1])
|
||||
return Path("case")
|
||||
|
||||
|
||||
def resolve_video_case_paths(video_case_dir: str | Path) -> tuple[Path, Path, Path]:
|
||||
case_dir = _normalize_video_case_input(video_case_dir)
|
||||
if not case_dir.is_dir():
|
||||
raise FileNotFoundError(f"Video case directory not found: {case_dir}")
|
||||
|
||||
video_path = case_dir / "sigmastar.1" / "camera4.bin"
|
||||
if not video_path.is_file():
|
||||
raise FileNotFoundError(f"camera4.bin not found under {case_dir}")
|
||||
|
||||
calib_candidates = [
|
||||
case_dir / "test_data" / "calibs" / "camera4.json",
|
||||
case_dir.parent / "test_data" / "calibs" / "camera4.json",
|
||||
case_dir / "sigmastar.1" / "calibs" / "camera4.json",
|
||||
case_dir / "calibs" / "camera4.json",
|
||||
]
|
||||
calib_path = next((path for path in calib_candidates if path.is_file()), None)
|
||||
if calib_path is None:
|
||||
checked = ", ".join(str(path) for path in calib_candidates)
|
||||
raise FileNotFoundError(f"camera4.json not found for {case_dir}. Checked: {checked}")
|
||||
|
||||
return case_dir, video_path, calib_path
|
||||
|
||||
|
||||
def collect_video_case_dirs(video_root_dir: str | Path) -> list[Path]:
|
||||
root_dir = Path(video_root_dir).resolve()
|
||||
if not root_dir.is_dir():
|
||||
raise FileNotFoundError(f"Video root directory not found: {root_dir}")
|
||||
|
||||
case_dirs: list[Path] = []
|
||||
for path in sorted(root_dir.iterdir()):
|
||||
if not path.is_dir():
|
||||
continue
|
||||
try:
|
||||
resolve_video_case_paths(path)
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
case_dirs.append(path)
|
||||
|
||||
if not case_dirs:
|
||||
raise FileNotFoundError(f"No valid video case directories found under {root_dir}")
|
||||
return case_dirs
|
||||
|
||||
|
||||
def read_video_frame_index(video_path: str | Path) -> Optional[dict[str, Any]]:
|
||||
try:
|
||||
video_path_obj = Path(video_path).resolve()
|
||||
case_dir = video_path_obj.parent.parent
|
||||
video_folder = video_path_obj.parent.name
|
||||
video_file = video_path_obj.stem
|
||||
index_path = case_dir / "L2" / f"{video_folder}.{video_file}.index.json"
|
||||
if not index_path.is_file():
|
||||
return None
|
||||
with index_path.open("r", encoding="utf-8") as file:
|
||||
payload = json.load(file)
|
||||
return payload if isinstance(payload, dict) else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def get_video_frame_info(frame_index_payload: Optional[dict[str, Any]], frame_idx: int) -> Optional[dict[str, Any]]:
|
||||
if not frame_index_payload:
|
||||
return None
|
||||
|
||||
try:
|
||||
fields = frame_index_payload.get("fields", {})
|
||||
index_list = frame_index_payload.get("index", [])
|
||||
if not isinstance(fields, dict) or not isinstance(index_list, list) or frame_idx >= len(index_list):
|
||||
return None
|
||||
frame_data = index_list[frame_idx]
|
||||
if not isinstance(frame_data, (list, tuple)):
|
||||
return None
|
||||
frame_info = {}
|
||||
for field_name, field_idx in fields.items():
|
||||
if isinstance(field_idx, int) and 0 <= field_idx < len(frame_data):
|
||||
frame_info[str(field_name)] = frame_data[field_idx]
|
||||
return frame_info
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _normalize_frame_info_token(value: Any) -> str:
|
||||
token = str(value or "").strip()
|
||||
if not token or token.lower() == "none":
|
||||
return ""
|
||||
return token.replace("/", "_").replace("\\", "_").replace(" ", "")
|
||||
|
||||
|
||||
def _safe_int(value: Any) -> Optional[int]:
|
||||
try:
|
||||
if value is None:
|
||||
return None
|
||||
return int(str(value).strip())
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def get_video_frame_id(frame_info: Optional[dict[str, Any]]) -> Optional[int]:
|
||||
if not frame_info:
|
||||
return None
|
||||
for key in ("frame_id", "cve_frame_id", "frameId"):
|
||||
frame_id = _safe_int(frame_info.get(key))
|
||||
if frame_id is not None:
|
||||
return frame_id
|
||||
return None
|
||||
|
||||
|
||||
def iter_video_case_frames(
|
||||
video_path: str | Path,
|
||||
*,
|
||||
frame_index_payload: Optional[dict[str, Any]] = None,
|
||||
frame_stride: int = 1,
|
||||
max_frames: int = 0,
|
||||
frame_index_start: Optional[int] = None,
|
||||
frame_index_end: Optional[int] = None,
|
||||
frame_id_start: Optional[int] = None,
|
||||
frame_id_end: Optional[int] = None,
|
||||
) -> Iterable[tuple[int, np.ndarray, str, Optional[dict[str, Any]]]]:
|
||||
resolved_video_path = Path(video_path).resolve()
|
||||
cap = cv2.VideoCapture(str(resolved_video_path))
|
||||
if not cap.isOpened():
|
||||
raise RuntimeError(f"Failed to open video file: {resolved_video_path}")
|
||||
|
||||
stride = max(1, int(frame_stride))
|
||||
resolved_frame_index_start = None if frame_index_start is None else max(0, int(frame_index_start))
|
||||
resolved_frame_index_end = None if frame_index_end is None else int(frame_index_end)
|
||||
resolved_frame_id_start = None if frame_id_start is None else int(frame_id_start)
|
||||
resolved_frame_id_end = None if frame_id_end is None else int(frame_id_end)
|
||||
read_frame_index = 0
|
||||
emitted_count = 0
|
||||
try:
|
||||
while True:
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
break
|
||||
|
||||
if resolved_frame_index_end is not None and read_frame_index > resolved_frame_index_end:
|
||||
break
|
||||
|
||||
frame_info = get_video_frame_info(frame_index_payload, read_frame_index)
|
||||
frame_id_value = get_video_frame_id(frame_info)
|
||||
|
||||
if resolved_frame_index_start is not None and read_frame_index < resolved_frame_index_start:
|
||||
read_frame_index += 1
|
||||
continue
|
||||
|
||||
if resolved_frame_id_start is not None:
|
||||
if frame_id_value is None or frame_id_value < resolved_frame_id_start:
|
||||
read_frame_index += 1
|
||||
continue
|
||||
|
||||
if resolved_frame_id_end is not None and frame_id_value is not None and frame_id_value > resolved_frame_id_end:
|
||||
break
|
||||
|
||||
if read_frame_index % stride != 0:
|
||||
read_frame_index += 1
|
||||
continue
|
||||
|
||||
frame_id_token = _normalize_frame_info_token(frame_id_value)
|
||||
timestamp_token = _normalize_frame_info_token(None if frame_info is None else frame_info.get("timestamp"))
|
||||
if frame_id_token and timestamp_token:
|
||||
frame_name = f"{resolved_video_path.stem}_{frame_id_token}_{timestamp_token}.png"
|
||||
elif frame_id_token:
|
||||
frame_name = f"{resolved_video_path.stem}_{frame_id_token}.png"
|
||||
elif timestamp_token:
|
||||
frame_name = f"{resolved_video_path.stem}_{read_frame_index:06d}_{timestamp_token}.png"
|
||||
else:
|
||||
frame_name = f"{resolved_video_path.stem}_{read_frame_index:06d}.png"
|
||||
|
||||
yield read_frame_index, frame, frame_name, frame_info
|
||||
emitted_count += 1
|
||||
read_frame_index += 1
|
||||
|
||||
if max_frames > 0 and emitted_count >= max_frames:
|
||||
break
|
||||
finally:
|
||||
cap.release()
|
||||
1
tools/model_inference/core/__init__.py
Executable file
1
tools/model_inference/core/__init__.py
Executable file
@@ -0,0 +1 @@
|
||||
"""Core inference modules for the self-contained two-ROI runtime."""
|
||||
1070
tools/model_inference/core/attribute_infer_utils.py
Executable file
1070
tools/model_inference/core/attribute_infer_utils.py
Executable file
File diff suppressed because it is too large
Load Diff
346
tools/model_inference/core/download_rawid_l2_by_event_json.py
Executable file
346
tools/model_inference/core/download_rawid_l2_by_event_json.py
Executable file
@@ -0,0 +1,346 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Download L2 raw packages referenced by scene-grouped event JSON records."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
except ImportError:
|
||||
def load_dotenv(*args: Any, **kwargs: Any) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
FILE = Path(__file__).resolve()
|
||||
ROOT = FILE.parents[3]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.append(str(ROOT))
|
||||
|
||||
from tools.model_inference.adapters.eventid_clip_resolver import ( # noqa: E402
|
||||
DEFAULT_EVENT_CACHE_FILE,
|
||||
ResolvedEventRecord,
|
||||
_extract_condition_values,
|
||||
_sanitize_identifier_for_path,
|
||||
_select_event_records_by_condition,
|
||||
resolve_event_records,
|
||||
)
|
||||
|
||||
|
||||
TIMESTAMP_RE = re.compile(r"^\d{14}$")
|
||||
|
||||
|
||||
@dataclass
|
||||
class DownloadResult:
|
||||
scene: str
|
||||
record_index: int
|
||||
rawid: str
|
||||
rawid_field_used: str
|
||||
condition_values: dict[str, str]
|
||||
l2_timestamp: str | None
|
||||
mdi_key: str | None
|
||||
source_data_path: str | None
|
||||
output_dir: str
|
||||
expected_download_dir: str | None
|
||||
status: str
|
||||
detail: str
|
||||
command: list[str] | None = None
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"scene": self.scene,
|
||||
"record_index": self.record_index,
|
||||
"rawid": self.rawid,
|
||||
"rawid_field_used": self.rawid_field_used,
|
||||
"condition_values": self.condition_values,
|
||||
"l2_timestamp": self.l2_timestamp,
|
||||
"mdi_key": self.mdi_key,
|
||||
"source_data_path": self.source_data_path,
|
||||
"output_dir": self.output_dir,
|
||||
"expected_download_dir": self.expected_download_dir,
|
||||
"status": self.status,
|
||||
"detail": self.detail,
|
||||
"command": self.command,
|
||||
}
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Download L2 data packages by resolving rawid metadata from an event JSON file."
|
||||
)
|
||||
parser.add_argument("--event-json-file", required=True)
|
||||
parser.add_argument("--scene", default="")
|
||||
parser.add_argument("--event-id-field", default="rawid")
|
||||
parser.add_argument("--event-clip-ids-field", default="clips")
|
||||
parser.add_argument("--condition-fields", nargs="*", default=[])
|
||||
parser.add_argument("--max-records-per-condition", type=int, default=0)
|
||||
parser.add_argument("--condition-select-strategy", default="first")
|
||||
parser.add_argument("--max-events", type=int, default=0)
|
||||
parser.add_argument("--event-cache-file", default=str(DEFAULT_EVENT_CACHE_FILE))
|
||||
parser.add_argument("--event-resolve-workers", type=int, default=4)
|
||||
parser.add_argument("--event-request-timeout", type=float, default=60.0)
|
||||
parser.add_argument("--event-request-retries", type=int, default=3)
|
||||
parser.add_argument("--event-request-retry-backoff-sec", type=float, default=2.0)
|
||||
parser.add_argument("--output-root", required=True)
|
||||
parser.add_argument("--manifest-path", default="")
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
parser.add_argument("--skip-done", action="store_true")
|
||||
parser.add_argument("--strict", action="store_true")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def ensure_pdcl_auth_defaults() -> None:
|
||||
load_dotenv()
|
||||
os.environ.setdefault("STS_UID", "dis-uploader")
|
||||
os.environ.setdefault("STS_SECRET_KEY", "277310cc09724d315514a79701fecb0f")
|
||||
|
||||
|
||||
def log_progress(message: str) -> None:
|
||||
timestamp = datetime.now().astimezone().strftime("%Y-%m-%d %H:%M:%S")
|
||||
print(f"[download_rawid_l2 {timestamp}] {message}", flush=True)
|
||||
|
||||
|
||||
def extract_l2_timestamp_from_meta(meta: dict[str, Any]) -> tuple[str, str]:
|
||||
data_info = meta.get("data_info")
|
||||
if isinstance(data_info, dict):
|
||||
items = data_info.get("items")
|
||||
if isinstance(items, list):
|
||||
for item in items:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
if str(item.get("data_type", "")).strip() != "onboard":
|
||||
continue
|
||||
if item.get("available") is False:
|
||||
continue
|
||||
data_path = str(item.get("data_path", "")).strip()
|
||||
timestamp = Path(data_path).stem
|
||||
if TIMESTAMP_RE.fullmatch(timestamp):
|
||||
return timestamp, data_path
|
||||
|
||||
store_path = str(meta.get("store_path", "")).strip()
|
||||
timestamp = Path(store_path).stem
|
||||
if TIMESTAMP_RE.fullmatch(timestamp):
|
||||
return timestamp, store_path
|
||||
|
||||
return "", ""
|
||||
|
||||
|
||||
def load_raw_meta(rawid: str) -> dict[str, Any]:
|
||||
ensure_pdcl_auth_defaults()
|
||||
from pdcl_dss import Raw
|
||||
|
||||
with Raw(rawid) as raw:
|
||||
return dict(raw.meta)
|
||||
|
||||
|
||||
def build_output_dir(output_root: Path, record: ResolvedEventRecord) -> Path:
|
||||
rawid_dir = _sanitize_identifier_for_path(record.event_id, prefix=record.event_id_field_used or "rawid")
|
||||
return output_root / record.scene / rawid_dir
|
||||
|
||||
|
||||
def write_manifest(
|
||||
manifest_path: Path,
|
||||
*,
|
||||
args: argparse.Namespace,
|
||||
selected_records: list[ResolvedEventRecord],
|
||||
selection_summary: dict[str, Any],
|
||||
results: list[DownloadResult],
|
||||
) -> None:
|
||||
manifest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
payload = {
|
||||
"event_json_file": str(Path(args.event_json_file).resolve()),
|
||||
"scene": args.scene,
|
||||
"event_id_field": args.event_id_field,
|
||||
"event_clip_ids_field": args.event_clip_ids_field,
|
||||
"output_root": str(Path(args.output_root).resolve()),
|
||||
"dry_run": bool(args.dry_run),
|
||||
"skip_done": bool(args.skip_done),
|
||||
"strict": bool(args.strict),
|
||||
"selected_record_count": len(selected_records),
|
||||
"selection": selection_summary,
|
||||
"summary": summarize_results(results),
|
||||
"results": [result.to_dict() for result in results],
|
||||
}
|
||||
manifest_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
|
||||
|
||||
def summarize_results(results: list[DownloadResult]) -> dict[str, int]:
|
||||
summary: dict[str, int] = {}
|
||||
for result in results:
|
||||
summary[result.status] = summary.get(result.status, 0) + 1
|
||||
return dict(sorted(summary.items()))
|
||||
|
||||
|
||||
def run_mdi_download(mdi_key: str, output_dir: Path, expected_download_dir: Path, *, dry_run: bool, skip_done: bool) -> tuple[str, str, list[str]]:
|
||||
command = ["mdi", "raw", "-r", mdi_key, "-s", str(output_dir)]
|
||||
|
||||
if skip_done and expected_download_dir.exists():
|
||||
return "exists", f"target already exists: {expected_download_dir}", command
|
||||
|
||||
if dry_run:
|
||||
return "planned", f"would run {' '.join(command)}", command
|
||||
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
if shutil.which("mdi") is None:
|
||||
return "failed", "mdi command not found in PATH", command
|
||||
|
||||
completed = subprocess.run(
|
||||
command,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
if completed.returncode == 0:
|
||||
return "downloaded", completed.stdout.strip() or "mdi raw completed", command
|
||||
|
||||
detail = completed.stderr.strip() or completed.stdout.strip() or "mdi raw failed"
|
||||
return "failed", detail, command
|
||||
|
||||
|
||||
def download_one_record(
|
||||
record: ResolvedEventRecord,
|
||||
*,
|
||||
output_root: Path,
|
||||
condition_fields: list[str],
|
||||
dry_run: bool,
|
||||
skip_done: bool,
|
||||
) -> DownloadResult:
|
||||
rawid = record.event_id
|
||||
output_dir = build_output_dir(output_root, record)
|
||||
condition_values = _extract_condition_values(record.source_record, condition_fields)
|
||||
|
||||
try:
|
||||
meta = load_raw_meta(rawid)
|
||||
l2_timestamp, source_data_path = extract_l2_timestamp_from_meta(meta)
|
||||
except Exception as exc:
|
||||
return DownloadResult(
|
||||
scene=record.scene,
|
||||
record_index=record.record_index,
|
||||
rawid=rawid,
|
||||
rawid_field_used=record.event_id_field_used,
|
||||
condition_values=condition_values,
|
||||
l2_timestamp=None,
|
||||
mdi_key=None,
|
||||
source_data_path=None,
|
||||
output_dir=str(output_dir),
|
||||
expected_download_dir=None,
|
||||
status="failed_meta",
|
||||
detail=f"{type(exc).__name__}: {exc}",
|
||||
)
|
||||
|
||||
if not l2_timestamp:
|
||||
return DownloadResult(
|
||||
scene=record.scene,
|
||||
record_index=record.record_index,
|
||||
rawid=rawid,
|
||||
rawid_field_used=record.event_id_field_used,
|
||||
condition_values=condition_values,
|
||||
l2_timestamp=None,
|
||||
mdi_key=None,
|
||||
source_data_path=source_data_path or None,
|
||||
output_dir=str(output_dir),
|
||||
expected_download_dir=None,
|
||||
status="failed_no_l2_timestamp",
|
||||
detail="no onboard L2 timestamp was found in Raw.meta",
|
||||
)
|
||||
|
||||
mdi_key = f"{rawid}::{l2_timestamp}"
|
||||
expected_download_dir = output_dir / l2_timestamp
|
||||
status, detail, command = run_mdi_download(
|
||||
mdi_key,
|
||||
output_dir,
|
||||
expected_download_dir,
|
||||
dry_run=dry_run,
|
||||
skip_done=skip_done,
|
||||
)
|
||||
return DownloadResult(
|
||||
scene=record.scene,
|
||||
record_index=record.record_index,
|
||||
rawid=rawid,
|
||||
rawid_field_used=record.event_id_field_used,
|
||||
condition_values=condition_values,
|
||||
l2_timestamp=l2_timestamp,
|
||||
mdi_key=mdi_key,
|
||||
source_data_path=source_data_path,
|
||||
output_dir=str(output_dir),
|
||||
expected_download_dir=str(expected_download_dir),
|
||||
status=status,
|
||||
detail=detail,
|
||||
command=command,
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
condition_fields = [str(field).strip() for field in args.condition_fields if str(field).strip()]
|
||||
output_root = Path(args.output_root).resolve()
|
||||
manifest_path = (
|
||||
Path(args.manifest_path).resolve()
|
||||
if args.manifest_path
|
||||
else output_root / "download_manifest.json"
|
||||
)
|
||||
|
||||
resolved_records, _ = resolve_event_records(
|
||||
json_file=args.event_json_file,
|
||||
scene_filter=args.scene or None,
|
||||
event_id_field=args.event_id_field,
|
||||
clip_ids_field=args.event_clip_ids_field,
|
||||
max_events=args.max_events,
|
||||
timeout=args.event_request_timeout,
|
||||
cache_file=args.event_cache_file,
|
||||
workers=args.event_resolve_workers,
|
||||
max_retries=args.event_request_retries,
|
||||
retry_backoff_sec=args.event_request_retry_backoff_sec,
|
||||
)
|
||||
selected_records, selection_summary = _select_event_records_by_condition(
|
||||
resolved_records,
|
||||
condition_fields=condition_fields,
|
||||
max_records_per_condition=args.max_records_per_condition,
|
||||
selection_strategy=args.condition_select_strategy,
|
||||
)
|
||||
|
||||
log_progress(
|
||||
f"selected {len(selected_records)} rawid record(s) from {len(resolved_records)} resolved record(s)"
|
||||
)
|
||||
|
||||
results: list[DownloadResult] = []
|
||||
for index, record in enumerate(selected_records, start=1):
|
||||
log_progress(f"[{index}/{len(selected_records)}] {record.scene} {record.event_id}")
|
||||
result = download_one_record(
|
||||
record,
|
||||
output_root=output_root,
|
||||
condition_fields=condition_fields,
|
||||
dry_run=args.dry_run,
|
||||
skip_done=args.skip_done,
|
||||
)
|
||||
results.append(result)
|
||||
log_progress(f" -> {result.status}: {result.mdi_key or result.detail}")
|
||||
write_manifest(
|
||||
manifest_path,
|
||||
args=args,
|
||||
selected_records=selected_records,
|
||||
selection_summary=selection_summary,
|
||||
results=results,
|
||||
)
|
||||
|
||||
summary = summarize_results(results)
|
||||
log_progress(f"manifest: {manifest_path}")
|
||||
log_progress("summary: " + ", ".join(f"{key}={value}" for key, value in summary.items()))
|
||||
|
||||
if args.strict and any(result.status.startswith("failed") for result in results):
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1013
tools/model_inference/core/edge_yaw_utils.py
Executable file
1013
tools/model_inference/core/edge_yaw_utils.py
Executable file
File diff suppressed because it is too large
Load Diff
4468
tools/model_inference/core/run_two_roi_exported_onnx_infer.py
Executable file
4468
tools/model_inference/core/run_two_roi_exported_onnx_infer.py
Executable file
File diff suppressed because it is too large
Load Diff
921
tools/model_inference/core/two_roi_3d_utils.py
Executable file
921
tools/model_inference/core/two_roi_3d_utils.py
Executable file
@@ -0,0 +1,921 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import Any, Optional
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
try:
|
||||
from .two_roi_types import (
|
||||
Decoded3DPrediction,
|
||||
DecodedVisibleEdge,
|
||||
Prediction3DAttrs,
|
||||
ResizedCalib,
|
||||
)
|
||||
except ImportError:
|
||||
from two_roi_types import (
|
||||
Decoded3DPrediction,
|
||||
DecodedVisibleEdge,
|
||||
Prediction3DAttrs,
|
||||
ResizedCalib,
|
||||
)
|
||||
|
||||
|
||||
PROJECTION_Z_MIN = 0.1
|
||||
YAW_BIN_OFFSETS = (0.0, np.pi / 2, -np.pi / 2, np.pi)
|
||||
FACE_OFFSETS_41 = (0, 6, 12, 18)
|
||||
FACE_EDGE_OFFSETS_60 = (0, 15, 30, 45)
|
||||
FACE_CORNERS = {0: (4, 5, 6, 7), 1: (0, 1, 2, 3), 2: (1, 2, 5, 6), 3: (0, 3, 4, 7)}
|
||||
FACE_BOTTOM_EDGE_CORNERS = {0: (6, 7), 1: (2, 3), 2: (2, 6), 3: (3, 7)}
|
||||
FACE_CENTER_OFFSETS = {0: [1, 0.5, 0.5], 1: [0, 0.5, 0.5], 2: [0.5, 0.5, 1], 3: [0.5, 0.5, 0]}
|
||||
FACE_VISIBILITY_SCORE_THRESH = 0.05
|
||||
CUT_STATE_NORMAL = 0
|
||||
CUT_STATE_IN = 1
|
||||
CUT_STATE_OUT = 2
|
||||
FACE_COLORS = ((0, 0, 255), (255, 0, 0), (0, 255, 0), (0, 255, 255))
|
||||
|
||||
|
||||
def rotation_3d_in_axis(points, angles, axis=1):
|
||||
rot_sin = np.sin(angles)
|
||||
rot_cos = np.cos(angles)
|
||||
ones = np.ones_like(rot_cos)
|
||||
zeros = np.zeros_like(rot_cos)
|
||||
if axis == 1:
|
||||
rot_mat = np.stack(
|
||||
[
|
||||
np.stack([rot_cos, zeros, -rot_sin]),
|
||||
np.stack([zeros, ones, zeros]),
|
||||
np.stack([rot_sin, zeros, rot_cos]),
|
||||
]
|
||||
)
|
||||
elif axis == 2:
|
||||
rot_mat = np.stack(
|
||||
[
|
||||
np.stack([rot_cos, rot_sin, zeros]),
|
||||
np.stack([-rot_sin, rot_cos, zeros]),
|
||||
np.stack([zeros, zeros, ones]),
|
||||
]
|
||||
)
|
||||
elif axis == 0:
|
||||
rot_mat = np.stack(
|
||||
[
|
||||
np.stack([ones, zeros, zeros]),
|
||||
np.stack([zeros, rot_cos, rot_sin]),
|
||||
np.stack([zeros, -rot_sin, rot_cos]),
|
||||
]
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"axis should be in [0, 1, 2], got {axis}")
|
||||
return np.dot(points, rot_mat)
|
||||
|
||||
|
||||
def compute_3d_box_corners(center_3d, dimensions, rotation, face_type=-1):
|
||||
l, h, w = dimensions
|
||||
corners_norm = np.stack(np.unravel_index(np.arange(8), [2] * 3), axis=1).astype(np.float64)
|
||||
corners_norm = corners_norm[[0, 1, 3, 2, 4, 5, 7, 6]]
|
||||
corners_norm -= FACE_CENTER_OFFSETS.get(face_type, [0.5, 0.5, 0.5])
|
||||
corners = np.array([l, h, w]).reshape(1, 3) * corners_norm.reshape(8, 3)
|
||||
corners = rotation_3d_in_axis(corners, rotation, axis=1)
|
||||
corners += np.array(center_3d).reshape(1, 3)
|
||||
return corners
|
||||
|
||||
|
||||
def apply_fisheye_distortion(x, y, distort_coeffs):
|
||||
if distort_coeffs is None or len(distort_coeffs) < 4:
|
||||
return x, y
|
||||
k1, k2, k3, k4 = distort_coeffs[:4]
|
||||
r = np.sqrt(x * x + y * y)
|
||||
if r < 1e-8:
|
||||
return x, y
|
||||
theta = np.arctan(r)
|
||||
theta2 = theta * theta
|
||||
theta4 = theta2 * theta2
|
||||
theta6 = theta4 * theta2
|
||||
theta8 = theta4 * theta4
|
||||
theta_d = theta * (1 + k1 * theta2 + k2 * theta4 + k3 * theta6 + k4 * theta8)
|
||||
scale = theta_d / r
|
||||
return x * scale, y * scale
|
||||
|
||||
|
||||
def remove_fisheye_distortion(xd, yd, distort_coeffs, max_iter=20):
|
||||
if distort_coeffs is None or len(distort_coeffs) < 4:
|
||||
return xd, yd
|
||||
k1, k2, k3, k4 = distort_coeffs[:4]
|
||||
r_d = np.sqrt(xd * xd + yd * yd)
|
||||
if r_d < 1e-8:
|
||||
return xd, yd
|
||||
theta_d = r_d
|
||||
theta_d2 = theta_d * theta_d
|
||||
theta = theta_d / (1 + k1 * theta_d2)
|
||||
for _ in range(max_iter):
|
||||
theta2 = theta * theta
|
||||
theta4 = theta2 * theta2
|
||||
theta6 = theta4 * theta2
|
||||
theta8 = theta4 * theta4
|
||||
f = theta * (1 + k1 * theta2 + k2 * theta4 + k3 * theta6 + k4 * theta8) - theta_d
|
||||
f_prime = 1 + 3 * k1 * theta2 + 5 * k2 * theta4 + 7 * k3 * theta6 + 9 * k4 * theta8
|
||||
theta_new = theta - f / f_prime
|
||||
if abs(theta_new - theta) < 1e-8:
|
||||
theta = theta_new
|
||||
break
|
||||
theta = theta_new
|
||||
r = np.tan(theta)
|
||||
scale = r / r_d
|
||||
return xd * scale, yd * scale
|
||||
|
||||
|
||||
def project_3d_to_2d_with_distortion(points_3d, calib: ResizedCalib):
|
||||
fx, fy = calib["fx"], calib["fy"]
|
||||
cx, cy = calib["cx"], calib["cy"]
|
||||
distort_coeffs = calib.get("distort_coeffs", [])
|
||||
points_2d = np.full((len(points_3d), 2), np.nan)
|
||||
for index, (x, y, z) in enumerate(points_3d):
|
||||
if z > PROJECTION_Z_MIN:
|
||||
xn, yn = x / z, y / z
|
||||
xd, yd = apply_fisheye_distortion(xn, yn, distort_coeffs)
|
||||
points_2d[index] = [fx * xd + cx, fy * yd + cy]
|
||||
return points_2d
|
||||
|
||||
|
||||
def project_3d_to_2d_with_calib(points_3d, calib: ResizedCalib):
|
||||
fx, fy = calib["fx"], calib["fy"]
|
||||
cx, cy = calib["cx"], calib["cy"]
|
||||
points_2d = np.full((len(points_3d), 2), np.nan)
|
||||
for index, (x, y, z) in enumerate(points_3d):
|
||||
if z > PROJECTION_Z_MIN:
|
||||
points_2d[index] = [fx * x / z + cx, fy * y / z + cy]
|
||||
return points_2d
|
||||
|
||||
|
||||
def project_3d_to_2d(points_3d, calib: ResizedCalib):
|
||||
if calib is None:
|
||||
return np.full((len(points_3d), 2), np.nan)
|
||||
distort_coeffs = calib.get("distort_coeffs", [])
|
||||
if distort_coeffs is not None and len(distort_coeffs) >= 4:
|
||||
return project_3d_to_2d_with_distortion(points_3d, calib)
|
||||
return project_3d_to_2d_with_calib(points_3d, calib)
|
||||
|
||||
|
||||
def sample_3d_edge(p1, p2, num_samples=10):
|
||||
t = np.linspace(0.0, 1.0, num_samples, dtype=np.float64).reshape(-1, 1)
|
||||
return p1 + t * (p2 - p1)
|
||||
|
||||
|
||||
def _point_inside_image(point_2d, img_w, img_h):
|
||||
x, y = float(point_2d[0]), float(point_2d[1])
|
||||
return np.isfinite(x) and np.isfinite(y) and 0.0 <= x <= img_w - 1 and 0.0 <= y <= img_h - 1
|
||||
|
||||
|
||||
def _solve_edge_image_boundary_t(p0_2d, p1_2d, img_w, img_h):
|
||||
p0 = np.asarray(p0_2d, dtype=np.float64)
|
||||
p1 = np.asarray(p1_2d, dtype=np.float64)
|
||||
if not np.isfinite(p0).all() or not np.isfinite(p1).all():
|
||||
return None
|
||||
dx, dy = p1 - p0
|
||||
t_min, t_max = 0.0, 1.0
|
||||
for p, q in ((-dx, p0[0]), (dx, (img_w - 1) - p0[0]), (-dy, p0[1]), (dy, (img_h - 1) - p0[1])):
|
||||
if abs(p) < 1e-12:
|
||||
if q < 0:
|
||||
return None
|
||||
continue
|
||||
t = q / p
|
||||
if p < 0:
|
||||
t_min = max(t_min, t)
|
||||
else:
|
||||
t_max = min(t_max, t)
|
||||
if t_min > t_max:
|
||||
return None
|
||||
return t_min, t_max
|
||||
|
||||
|
||||
def _project_edge_point_at_t(p1, p2, t, calib: ResizedCalib):
|
||||
point_3d = np.asarray(p1, dtype=np.float64) + float(t) * (np.asarray(p2, dtype=np.float64) - np.asarray(p1, dtype=np.float64))
|
||||
point_2d = project_3d_to_2d(point_3d[None, :], calib)[0]
|
||||
return point_3d, point_2d
|
||||
|
||||
|
||||
def _refine_visible_edge_boundary(p1, p2, calib: ResizedCalib, img_w, img_h, t_out, t_in, steps=12):
|
||||
lo, hi = (float(t_out), float(t_in)) if t_out < t_in else (float(t_in), float(t_out))
|
||||
for _ in range(steps):
|
||||
mid = 0.5 * (lo + hi)
|
||||
_, point_2d = _project_edge_point_at_t(p1, p2, mid, calib)
|
||||
if _point_inside_image(point_2d, img_w, img_h):
|
||||
hi = mid
|
||||
else:
|
||||
lo = mid
|
||||
return hi if t_out < t_in else lo
|
||||
|
||||
|
||||
def sample_partial_3d_edge(p1, p2, calib: ResizedCalib, img_w, img_h, num_samples=5, dense_samples=129):
|
||||
endpoints_3d = np.asarray([p1, p2], dtype=np.float64)
|
||||
dense_t = np.linspace(0.0, 1.0, dense_samples, dtype=np.float64)
|
||||
dense_points_3d = endpoints_3d[0:1] + dense_t[:, None] * (endpoints_3d[1:2] - endpoints_3d[0:1])
|
||||
dense_points_2d = project_3d_to_2d(dense_points_3d, calib)
|
||||
visible = np.array([_point_inside_image(point_2d, img_w, img_h) for point_2d in dense_points_2d], dtype=bool)
|
||||
if not visible.any():
|
||||
return None, None
|
||||
|
||||
visible_idx = np.flatnonzero(visible)
|
||||
split_idx = np.where(np.diff(visible_idx) > 1)[0] + 1
|
||||
visible_runs = np.split(visible_idx, split_idx)
|
||||
visible_run = max(visible_runs, key=len)
|
||||
first_idx, last_idx = int(visible_run[0]), int(visible_run[-1])
|
||||
|
||||
t_start = dense_t[first_idx]
|
||||
if first_idx > 0:
|
||||
t_start = _refine_visible_edge_boundary(
|
||||
endpoints_3d[0],
|
||||
endpoints_3d[1],
|
||||
calib,
|
||||
img_w,
|
||||
img_h,
|
||||
dense_t[first_idx - 1],
|
||||
dense_t[first_idx],
|
||||
)
|
||||
|
||||
t_end = dense_t[last_idx]
|
||||
if last_idx < len(dense_t) - 1:
|
||||
t_end = _refine_visible_edge_boundary(
|
||||
endpoints_3d[0],
|
||||
endpoints_3d[1],
|
||||
calib,
|
||||
img_w,
|
||||
img_h,
|
||||
dense_t[last_idx + 1],
|
||||
dense_t[last_idx],
|
||||
)
|
||||
|
||||
if t_end - t_start < 1e-6:
|
||||
return None, None
|
||||
|
||||
sample_t = np.linspace(t_start, t_end, num_samples, dtype=np.float64)
|
||||
sample_points_3d = endpoints_3d[0:1] + sample_t[:, None] * (endpoints_3d[1:2] - endpoints_3d[0:1])
|
||||
sample_points_2d = project_3d_to_2d(sample_points_3d, calib)
|
||||
if np.any(np.isnan(sample_points_2d)):
|
||||
return None, None
|
||||
if not np.all([_point_inside_image(point_2d, img_w, img_h) for point_2d in sample_points_2d]):
|
||||
return None, None
|
||||
|
||||
order = np.argsort(sample_points_2d[:, 0], kind="stable")
|
||||
return sample_points_3d[order], sample_points_2d[order]
|
||||
|
||||
|
||||
def project_3d_box_edges_with_distortion(corners_3d, calib: ResizedCalib, samples_per_edge=10):
|
||||
edges = {
|
||||
"back_0": (4, 5),
|
||||
"back_1": (5, 6),
|
||||
"back_2": (6, 7),
|
||||
"back_3": (7, 4),
|
||||
"connect_0": (0, 4),
|
||||
"connect_1": (1, 5),
|
||||
"connect_2": (2, 6),
|
||||
"connect_3": (3, 7),
|
||||
"front_0": (0, 1),
|
||||
"front_1": (1, 2),
|
||||
"front_2": (2, 3),
|
||||
"front_3": (3, 0),
|
||||
"front_x1": (0, 2),
|
||||
"front_x2": (1, 3),
|
||||
}
|
||||
edge_points_2d = {}
|
||||
for edge_name, (i, j) in edges.items():
|
||||
sampled_3d = sample_3d_edge(corners_3d[i], corners_3d[j], samples_per_edge)
|
||||
edge_points_2d[edge_name] = project_3d_to_2d_with_distortion(sampled_3d, calib)
|
||||
return edge_points_2d
|
||||
|
||||
|
||||
def plot_box3d_on_img_with_distortion(
|
||||
img,
|
||||
edge_points_2d,
|
||||
color_front=(0, 0, 255),
|
||||
color_back=(255, 0, 0),
|
||||
color_side=(255, 255, 0),
|
||||
thickness=1,
|
||||
):
|
||||
front_edges = {"front_0", "front_1", "front_2", "front_3", "front_x1", "front_x2"}
|
||||
back_edges = {"back_0", "back_1", "back_2", "back_3", "back_x1", "back_x2"}
|
||||
for edge_name, points in edge_points_2d.items():
|
||||
if np.any(np.isnan(points)):
|
||||
continue
|
||||
pts = points.astype(np.int32)
|
||||
color = color_front if edge_name in front_edges else color_back if edge_name in back_edges else color_side
|
||||
cv2.polylines(img, [pts], isClosed=False, color=color, thickness=thickness, lineType=cv2.LINE_AA)
|
||||
return img
|
||||
|
||||
|
||||
def plot_box3d_on_img(img, corners_2d, color_front=(0, 0, 255), color_back=(255, 0, 0), color_side=(255, 255, 0), thickness=1):
|
||||
line_indices = (
|
||||
(4, 5),
|
||||
(5, 6),
|
||||
(6, 7),
|
||||
(7, 4),
|
||||
(0, 4),
|
||||
(1, 5),
|
||||
(2, 6),
|
||||
(3, 7),
|
||||
(0, 1),
|
||||
(1, 2),
|
||||
(2, 3),
|
||||
(3, 0),
|
||||
(0, 2),
|
||||
(1, 3),
|
||||
)
|
||||
front_edges = {(0, 1), (1, 2), (2, 3), (3, 0), (0, 2), (1, 3)}
|
||||
back_edges = {(4, 5), (5, 6), (6, 7), (7, 4)}
|
||||
pts = corners_2d.astype(np.int32)
|
||||
for i, j in line_indices:
|
||||
color = color_front if (i, j) in front_edges else color_back if (i, j) in back_edges else color_side
|
||||
cv2.line(img, tuple(pts[i]), tuple(pts[j]), color, thickness, cv2.LINE_AA)
|
||||
return img
|
||||
|
||||
|
||||
def back_project_2d_to_3d(uv, depth, calib: ResizedCalib):
|
||||
if calib is None or depth <= 0:
|
||||
return None
|
||||
fx, fy = calib["fx"], calib["fy"]
|
||||
cx, cy = calib["cx"], calib["cy"]
|
||||
u, v = uv
|
||||
xd = (u - cx) / fx
|
||||
yd = (v - cy) / fy
|
||||
distort_coeffs = calib.get("distort_coeffs", [])
|
||||
if distort_coeffs is not None and len(distort_coeffs) >= 4:
|
||||
xn, yn = remove_fisheye_distortion(xd, yd, distort_coeffs)
|
||||
else:
|
||||
xn, yn = xd, yd
|
||||
return np.array([xn * depth, yn * depth, depth], dtype=np.float64)
|
||||
|
||||
|
||||
def reconstruct_3d_box_from_face(face_uv, face_z, dims, rot_y, face_type, calib: ResizedCalib):
|
||||
if calib is None or face_z <= 0:
|
||||
return None
|
||||
center_3d = back_project_2d_to_3d(face_uv, face_z, calib)
|
||||
if center_3d is None:
|
||||
return None
|
||||
if np.any(np.isnan(np.asarray(dims, dtype=np.float64))) or not np.isfinite(float(rot_y)):
|
||||
return None
|
||||
return compute_3d_box_corners(center_3d, dims, rot_y, face_type)
|
||||
|
||||
|
||||
def reconstruct_3d_box_from_whole(uv, z3d, dims, rot_y, calib: ResizedCalib):
|
||||
if calib is None or z3d <= 0:
|
||||
return None
|
||||
center_3d = back_project_2d_to_3d(uv, z3d, calib)
|
||||
if center_3d is None:
|
||||
return None
|
||||
if np.any(np.isnan(np.asarray(dims, dtype=np.float64))) or not np.isfinite(float(rot_y)):
|
||||
return None
|
||||
return compute_3d_box_corners(center_3d, dims, rot_y, face_type=-1)
|
||||
|
||||
|
||||
def get_face_bottom_edge_points(corners_3d, face_type, num_samples=5):
|
||||
if corners_3d is None or face_type not in FACE_BOTTOM_EDGE_CORNERS:
|
||||
return None
|
||||
start_idx, end_idx = FACE_BOTTOM_EDGE_CORNERS[face_type]
|
||||
return sample_3d_edge(corners_3d[start_idx], corners_3d[end_idx], num_samples=num_samples)
|
||||
|
||||
|
||||
def project_face_bottom_edge(corners_3d, face_type, calib: ResizedCalib, num_samples=5):
|
||||
points_3d = get_face_bottom_edge_points(corners_3d, face_type, num_samples=num_samples)
|
||||
if points_3d is None:
|
||||
return None, None
|
||||
points_2d = project_3d_to_2d(points_3d, calib)
|
||||
if np.any(np.isnan(points_2d)):
|
||||
return points_3d, None
|
||||
order = np.argsort(points_2d[:, 0], kind="stable")
|
||||
return points_3d[order], points_2d[order]
|
||||
|
||||
|
||||
def project_partial_face_bottom_edge(corners_3d, face_type, calib: ResizedCalib, img_w, img_h, num_samples=5):
|
||||
if corners_3d is None or face_type not in FACE_BOTTOM_EDGE_CORNERS:
|
||||
return None, None
|
||||
start_idx, end_idx = FACE_BOTTOM_EDGE_CORNERS[face_type]
|
||||
return sample_partial_3d_edge(corners_3d[start_idx], corners_3d[end_idx], calib, img_w, img_h, num_samples=num_samples)
|
||||
|
||||
|
||||
def collect_face_bottom_edges(corners_3d, face_types, calib: ResizedCalib, num_samples=5):
|
||||
if corners_3d is None:
|
||||
return None, None
|
||||
edge_points_3d, edge_points_2d = [], []
|
||||
for face_type in face_types:
|
||||
points_3d, points_2d = project_face_bottom_edge(corners_3d, face_type, calib, num_samples=num_samples)
|
||||
if points_3d is None or points_2d is None:
|
||||
continue
|
||||
edge_points_3d.append(points_3d.astype(np.float32, copy=False))
|
||||
edge_points_2d.append(points_2d.astype(np.float32, copy=False))
|
||||
if not edge_points_2d:
|
||||
return None, None
|
||||
if len(edge_points_2d) == 1:
|
||||
return edge_points_3d[0], edge_points_2d[0]
|
||||
return np.stack(edge_points_3d, axis=0), np.stack(edge_points_2d, axis=0)
|
||||
|
||||
|
||||
def _edge_batches_to_list(edge_points):
|
||||
if edge_points is None:
|
||||
return []
|
||||
arr = np.asarray(edge_points, dtype=np.float32)
|
||||
if arr.ndim == 2:
|
||||
return [arr]
|
||||
return [arr[i] for i in range(arr.shape[0])]
|
||||
|
||||
|
||||
def _stack_edge_batches(edge_batches):
|
||||
if not edge_batches:
|
||||
return None
|
||||
if len(edge_batches) == 1:
|
||||
return edge_batches[0]
|
||||
return np.stack(edge_batches, axis=0)
|
||||
|
||||
|
||||
def _append_edge_batch(edge_points_3d, edge_points_2d, decoded_edge: DecodedVisibleEdge):
|
||||
if decoded_edge.points_3d is None:
|
||||
return edge_points_3d, edge_points_2d
|
||||
edge3d_list = _edge_batches_to_list(edge_points_3d)
|
||||
edge2d_list = _edge_batches_to_list(edge_points_2d)
|
||||
edge3d_list.append(np.asarray(decoded_edge.points_3d, dtype=np.float32))
|
||||
edge2d_list.append(np.asarray(decoded_edge.points_2d, dtype=np.float32))
|
||||
return _stack_edge_batches(edge3d_list), _stack_edge_batches(edge2d_list)
|
||||
|
||||
|
||||
def decode_visible_face_edge_from_prediction(pred_edge_60, face_type, anchor_xy, stride) -> Optional[DecodedVisibleEdge]:
|
||||
if pred_edge_60 is None or face_type not in range(4):
|
||||
return None
|
||||
off = FACE_EDGE_OFFSETS_60[face_type]
|
||||
face = np.asarray(pred_edge_60[off : off + 15], dtype=np.float32).reshape(5, 3)
|
||||
points_2d = np.empty((5, 2), dtype=np.float32)
|
||||
points_2d[:, 0] = (anchor_xy[0] + face[:, 0]) * stride
|
||||
points_2d[:, 1] = (anchor_xy[1] + face[:, 1]) * stride
|
||||
order = np.argsort(points_2d[:, 0], kind="stable")
|
||||
return DecodedVisibleEdge(
|
||||
face_type=int(face_type),
|
||||
points_2d=points_2d[order],
|
||||
depths=face[order, 2].astype(np.float32),
|
||||
)
|
||||
|
||||
|
||||
def get_cut_side_from_bbox_xyxy(bbox_xyxy, img_w, tol=1.0):
|
||||
if bbox_xyxy is None:
|
||||
return None
|
||||
x1, _, x2, _ = np.asarray(bbox_xyxy, dtype=np.float64)
|
||||
touch_left = x1 <= tol and x2 > tol
|
||||
touch_right = x2 >= img_w - 1 - tol and x1 < img_w - 1 - tol
|
||||
if touch_left == touch_right:
|
||||
return None
|
||||
return "left" if touch_left else "right"
|
||||
|
||||
|
||||
def get_cut_object_side_face(face_type_or_state, cut_side=None):
|
||||
if cut_side not in {"left", "right"}:
|
||||
return None
|
||||
if face_type_or_state not in {CUT_STATE_IN, CUT_STATE_OUT}:
|
||||
return None
|
||||
return 3 if cut_side == "left" else 2
|
||||
|
||||
|
||||
def get_pred_cut_state(pred_41):
|
||||
cut_logits = np.asarray(pred_41[38:41], dtype=np.float32)
|
||||
return int(np.argmax(cut_logits))
|
||||
|
||||
|
||||
def get_pred_cut_primary_face(cut_state):
|
||||
if cut_state == CUT_STATE_IN:
|
||||
return 0
|
||||
if cut_state == CUT_STATE_OUT:
|
||||
return 1
|
||||
return None
|
||||
|
||||
|
||||
def _resolve_pred_cut_state_for_decode(pred_41, bbox_xyxy=None, img_w=None):
|
||||
cut_state = get_pred_cut_state(pred_41)
|
||||
if cut_state == CUT_STATE_NORMAL:
|
||||
return cut_state, None
|
||||
cut_side = None
|
||||
if bbox_xyxy is not None and img_w is not None:
|
||||
cut_side = get_cut_side_from_bbox_xyxy(bbox_xyxy, img_w)
|
||||
if cut_side not in {"left", "right"}:
|
||||
return CUT_STATE_NORMAL, None
|
||||
return cut_state, cut_side
|
||||
|
||||
|
||||
def select_pred_visible_faces(pred_41, score_thr=FACE_VISIBILITY_SCORE_THRESH):
|
||||
selected = []
|
||||
for face_type, off in enumerate(FACE_OFFSETS_41):
|
||||
score = float(pred_41[off + 5])
|
||||
if np.isnan(score) or score < score_thr:
|
||||
continue
|
||||
selected.append((face_type, score))
|
||||
return selected
|
||||
|
||||
|
||||
def _select_best_pred_face_score(pred_41):
|
||||
best_face_type, best_score = None, float("-inf")
|
||||
for face_type, off in enumerate(FACE_OFFSETS_41):
|
||||
score = float(pred_41[off + 5])
|
||||
if not np.isfinite(score):
|
||||
continue
|
||||
if score > best_score:
|
||||
best_face_type = int(face_type)
|
||||
best_score = float(score)
|
||||
if best_face_type is None:
|
||||
return None
|
||||
return best_face_type, best_score
|
||||
|
||||
|
||||
def select_pred_visible_faces_for_decode(pred_41, score_thr=FACE_VISIBILITY_SCORE_THRESH, bbox_xyxy=None, img_w=None):
|
||||
cut_state, _ = _resolve_pred_cut_state_for_decode(pred_41, bbox_xyxy=bbox_xyxy, img_w=img_w)
|
||||
primary_face = get_pred_cut_primary_face(cut_state)
|
||||
if primary_face is not None:
|
||||
off = FACE_OFFSETS_41[primary_face]
|
||||
return [(primary_face, float(pred_41[off + 5]))]
|
||||
visible_faces = list(select_pred_visible_faces(pred_41, score_thr=score_thr))
|
||||
best_face = _select_best_pred_face_score(pred_41)
|
||||
if best_face is None:
|
||||
return visible_faces
|
||||
best_face_type, best_score = best_face
|
||||
if all(int(face_type) != int(best_face_type) for face_type, _ in visible_faces):
|
||||
visible_faces.append((int(best_face_type), float(best_score)))
|
||||
return visible_faces
|
||||
|
||||
|
||||
def decode_cut_partial_side_edge_from_prediction(pred_41, pred_edge_60, anchor_xy, stride, img_w, cut_side=None) -> Optional[DecodedVisibleEdge]:
|
||||
if pred_edge_60 is None:
|
||||
return None
|
||||
cut_state = get_pred_cut_state(pred_41)
|
||||
if cut_state == CUT_STATE_NORMAL:
|
||||
return None
|
||||
side_face_type = get_cut_object_side_face(cut_state, cut_side)
|
||||
if side_face_type is None:
|
||||
return None
|
||||
return decode_visible_face_edge_from_prediction(pred_edge_60, side_face_type, anchor_xy, stride)
|
||||
|
||||
|
||||
def _decoded_edge_to_points_3d(decoded_edge: Optional[DecodedVisibleEdge], calib: ResizedCalib) -> Optional[np.ndarray]:
|
||||
if decoded_edge is None:
|
||||
return None
|
||||
points = [back_project_2d_to_3d(tuple(pt), depth, calib) for pt, depth in zip(decoded_edge.points_2d, decoded_edge.depths)]
|
||||
if any(point is None for point in points):
|
||||
return None
|
||||
return np.asarray(points, dtype=np.float32)
|
||||
|
||||
|
||||
def edge_points_to_yaw(points_3d, face_type):
|
||||
points = np.asarray(points_3d, dtype=np.float32)
|
||||
if points.shape[0] < 2:
|
||||
return float("nan")
|
||||
direction = points[-1] - points[0]
|
||||
yaw = math.atan2(float(direction[0]), float(direction[2]))
|
||||
if face_type == 0:
|
||||
return yaw
|
||||
if face_type == 1:
|
||||
return yaw + np.pi
|
||||
if face_type == 2:
|
||||
return yaw + np.pi / 2
|
||||
if face_type == 3:
|
||||
return yaw - np.pi / 2
|
||||
return float("nan")
|
||||
|
||||
|
||||
def visible_face_edges_to_yaw(face_edges_3d, face_scores=None):
|
||||
if not face_edges_3d:
|
||||
return float("nan")
|
||||
if face_scores:
|
||||
face_type = max(face_edges_3d.keys(), key=lambda ft: face_scores.get(ft, 0.0))
|
||||
return edge_points_to_yaw(face_edges_3d[face_type], face_type)
|
||||
face_type = next(iter(face_edges_3d))
|
||||
return edge_points_to_yaw(face_edges_3d[face_type], face_type)
|
||||
|
||||
|
||||
def _draw_edge_points(img, edge_points_2d=None, edge_color=(0, 255, 0), thickness=1):
|
||||
if edge_points_2d is None:
|
||||
return
|
||||
points = np.asarray(edge_points_2d, dtype=np.float32)
|
||||
if points.ndim == 2:
|
||||
points = points[None, ...]
|
||||
for batch in points:
|
||||
for point in batch:
|
||||
cv2.circle(img, tuple(np.round(point).astype(np.int32)), max(thickness + 1, 2), edge_color, -1, cv2.LINE_AA)
|
||||
|
||||
|
||||
def _decode_yaw_from_prediction(pred_41):
|
||||
yaw_cls_logits = pred_41[30:34]
|
||||
yaw_residual_sin = np.clip(pred_41[34:38], -1.0, 1.0)
|
||||
best_bin = int(np.argmax(yaw_cls_logits))
|
||||
return np.arcsin(yaw_residual_sin[best_bin]) + YAW_BIN_OFFSETS[best_bin]
|
||||
|
||||
|
||||
def decode_3d_prediction(
|
||||
pred_41,
|
||||
anchor_xy,
|
||||
stride,
|
||||
calib,
|
||||
img_w,
|
||||
img_h,
|
||||
face_3d_classes,
|
||||
complete_3d_classes,
|
||||
cls_id,
|
||||
pred_edge_60=None,
|
||||
score_thr=FACE_VISIBILITY_SCORE_THRESH,
|
||||
bbox_xyxy=None,
|
||||
) -> Optional[Decoded3DPrediction]:
|
||||
pred = pred_41
|
||||
rot_y = _decode_yaw_from_prediction(pred)
|
||||
z_whole = pred[24]
|
||||
uv_whole_offset = pred[25:27]
|
||||
dims_whole = pred[27:30]
|
||||
u_whole = (anchor_xy[0] + uv_whole_offset[0]) * stride
|
||||
v_whole = (anchor_xy[1] + uv_whole_offset[1]) * stride
|
||||
|
||||
if cls_id in face_3d_classes:
|
||||
_, cut_side = _resolve_pred_cut_state_for_decode(pred, bbox_xyxy=bbox_xyxy, img_w=img_w)
|
||||
visible_faces = select_pred_visible_faces_for_decode(pred, score_thr=score_thr, bbox_xyxy=bbox_xyxy, img_w=img_w)
|
||||
best_type, _ = (-1, -1.0) if not visible_faces else max(visible_faces, key=lambda item: item[1])
|
||||
if best_type < 0:
|
||||
return None
|
||||
|
||||
off = best_type * 6
|
||||
z_face = pred[off]
|
||||
uv_face_offset = pred[off + 1 : off + 3]
|
||||
u_face = (anchor_xy[0] + uv_face_offset[0]) * stride
|
||||
v_face = (anchor_xy[1] + uv_face_offset[1]) * stride
|
||||
corners = reconstruct_3d_box_from_face((u_face, v_face), z_face, dims_whole, rot_y, best_type, calib)
|
||||
if corners is None:
|
||||
return None
|
||||
|
||||
edge_points_3d, edge_points_2d = collect_face_bottom_edges(
|
||||
corners,
|
||||
[face_type for face_type, _ in visible_faces],
|
||||
calib,
|
||||
num_samples=5,
|
||||
)
|
||||
if pred_edge_60 is not None:
|
||||
pred_edge_points_2d, pred_edge_points_3d = [], []
|
||||
for face_type, _ in visible_faces:
|
||||
pred_edge = decode_visible_face_edge_from_prediction(pred_edge_60, face_type, anchor_xy, stride)
|
||||
if pred_edge is None:
|
||||
continue
|
||||
points_3d = [back_project_2d_to_3d(tuple(pt), depth, calib) for pt, depth in zip(pred_edge.points_2d, pred_edge.depths)]
|
||||
if any(point is None for point in points_3d):
|
||||
continue
|
||||
pred_edge_points_2d.append(pred_edge.points_2d.astype(np.float32, copy=False))
|
||||
pred_edge_points_3d.append(np.asarray(points_3d, dtype=np.float32))
|
||||
if pred_edge_points_2d:
|
||||
edge_points_2d = _stack_edge_batches(pred_edge_points_2d)
|
||||
edge_points_3d = _stack_edge_batches(pred_edge_points_3d)
|
||||
|
||||
partial_edge = decode_cut_partial_side_edge_from_prediction(
|
||||
pred,
|
||||
pred_edge_60,
|
||||
anchor_xy,
|
||||
stride,
|
||||
img_w,
|
||||
cut_side=cut_side,
|
||||
)
|
||||
if partial_edge is not None:
|
||||
partial_points_3d = [back_project_2d_to_3d(tuple(pt), depth, calib) for pt, depth in zip(partial_edge.points_2d, partial_edge.depths)]
|
||||
if all(point is not None for point in partial_points_3d):
|
||||
partial_edge.points_3d = np.asarray(partial_points_3d, dtype=np.float32)
|
||||
visible_face_types = {face_type for face_type, _ in visible_faces}
|
||||
if partial_edge.face_type not in visible_face_types:
|
||||
edge_points_3d, edge_points_2d = _append_edge_batch(edge_points_3d, edge_points_2d, partial_edge)
|
||||
visible_faces = [*visible_faces, (partial_edge.face_type, 1.0)]
|
||||
|
||||
return Decoded3DPrediction(
|
||||
corners_3d=np.asarray(corners, dtype=np.float32),
|
||||
face_center_2d=(u_face, v_face),
|
||||
face_color=FACE_COLORS[best_type],
|
||||
visible_face_type=best_type,
|
||||
visible_face_types=tuple(face_type for face_type, _ in visible_faces),
|
||||
edge_points_2d=None if edge_points_2d is None else np.asarray(edge_points_2d, dtype=np.float32),
|
||||
edge_points_3d=None if edge_points_3d is None else np.asarray(edge_points_3d, dtype=np.float32),
|
||||
cls_id=cls_id,
|
||||
)
|
||||
|
||||
if cls_id in complete_3d_classes:
|
||||
corners = reconstruct_3d_box_from_whole((u_whole, v_whole), z_whole, dims_whole, rot_y, calib)
|
||||
if corners is None:
|
||||
return None
|
||||
return Decoded3DPrediction(
|
||||
corners_3d=np.asarray(corners, dtype=np.float32),
|
||||
face_center_2d=None,
|
||||
face_color=None,
|
||||
visible_face_type=None,
|
||||
visible_face_types=(),
|
||||
edge_points_2d=None,
|
||||
edge_points_3d=None,
|
||||
cls_id=cls_id,
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def draw_3d_box(img, corners_3d, calib: ResizedCalib, face_center_2d=None, face_color=None, edge_points_2d=None, edge_color=(0, 255, 0), thickness=1):
|
||||
corners_3d = corners_3d[[4, 5, 6, 7, 0, 1, 2, 3]]
|
||||
color_front = (0, 0, 255)
|
||||
color_back = (255, 0, 0)
|
||||
color_side = (255, 255, 0)
|
||||
|
||||
distort_coeffs = calib.get("distort_coeffs", []) if calib is not None else []
|
||||
if distort_coeffs is not None and len(distort_coeffs) >= 4:
|
||||
edge_points_2d_box = project_3d_box_edges_with_distortion(corners_3d, calib, samples_per_edge=15)
|
||||
plot_box3d_on_img_with_distortion(
|
||||
img,
|
||||
edge_points_2d_box,
|
||||
color_front=color_front,
|
||||
color_back=color_back,
|
||||
color_side=color_side,
|
||||
thickness=thickness,
|
||||
)
|
||||
else:
|
||||
corners_2d = project_3d_to_2d(corners_3d, calib)
|
||||
if np.any(np.isnan(corners_2d)):
|
||||
return img
|
||||
plot_box3d_on_img(
|
||||
img,
|
||||
corners_2d,
|
||||
color_front=color_front,
|
||||
color_back=color_back,
|
||||
color_side=color_side,
|
||||
thickness=thickness,
|
||||
)
|
||||
|
||||
if face_center_2d is not None and face_color is not None:
|
||||
cv2.circle(img, (int(face_center_2d[0]), int(face_center_2d[1])), 2, face_color, -1, cv2.LINE_AA)
|
||||
|
||||
_draw_edge_points(img, edge_points_2d=edge_points_2d, edge_color=edge_color, thickness=thickness)
|
||||
return img
|
||||
|
||||
|
||||
def decode_visible_face_yaw_from_prediction(pred_41, pred_edge_60, anchor_xy, stride, face_type, calib: ResizedCalib):
|
||||
if pred_edge_60 is None or face_type not in range(4):
|
||||
return float("nan")
|
||||
decoded = decode_visible_face_edge_from_prediction(pred_edge_60, face_type, anchor_xy, stride)
|
||||
points_3d = _decoded_edge_to_points_3d(decoded, calib)
|
||||
if points_3d is None:
|
||||
return float("nan")
|
||||
return edge_points_to_yaw(points_3d, face_type)
|
||||
|
||||
|
||||
def decode_multi_visible_face_yaw_from_prediction(
|
||||
pred_41,
|
||||
pred_edge_60,
|
||||
anchor_xy,
|
||||
stride,
|
||||
calib,
|
||||
fallback_face_type=None,
|
||||
score_thr=FACE_VISIBILITY_SCORE_THRESH,
|
||||
bbox_xyxy=None,
|
||||
img_w=None,
|
||||
):
|
||||
if pred_edge_60 is None:
|
||||
if fallback_face_type in range(4):
|
||||
return decode_visible_face_yaw_from_prediction(pred_41, pred_edge_60, anchor_xy, stride, fallback_face_type, calib)
|
||||
return float("nan")
|
||||
|
||||
inferred_img_w = float(img_w) if img_w is not None else None
|
||||
if inferred_img_w is None:
|
||||
if bbox_xyxy is not None:
|
||||
inferred_img_w = max(float(np.asarray(bbox_xyxy, dtype=np.float64)[2]), 1.0)
|
||||
else:
|
||||
inferred_img_w = max(float((anchor_xy[0] + pred_41[25]) * stride) * 2.0, 1.0)
|
||||
|
||||
cut_state, cut_side = _resolve_pred_cut_state_for_decode(pred_41, bbox_xyxy=bbox_xyxy, img_w=inferred_img_w)
|
||||
face_edges_3d, face_scores = {}, {}
|
||||
for face_type, score in select_pred_visible_faces_for_decode(pred_41, score_thr=score_thr, bbox_xyxy=bbox_xyxy, img_w=inferred_img_w):
|
||||
decoded = decode_visible_face_edge_from_prediction(pred_edge_60, face_type, anchor_xy, stride)
|
||||
points_3d = _decoded_edge_to_points_3d(decoded, calib)
|
||||
if points_3d is None:
|
||||
continue
|
||||
face_edges_3d[face_type] = points_3d
|
||||
face_scores[face_type] = float(score)
|
||||
|
||||
partial_edge = decode_cut_partial_side_edge_from_prediction(
|
||||
pred_41,
|
||||
pred_edge_60,
|
||||
anchor_xy,
|
||||
stride,
|
||||
img_w=inferred_img_w,
|
||||
cut_side=cut_side,
|
||||
)
|
||||
partial_points_3d = _decoded_edge_to_points_3d(partial_edge, calib)
|
||||
if cut_state != CUT_STATE_NORMAL:
|
||||
if partial_edge is not None and partial_points_3d is not None:
|
||||
return edge_points_to_yaw(partial_points_3d, int(partial_edge.face_type))
|
||||
return float("nan")
|
||||
|
||||
if any(face_type in (2, 3) for face_type in face_edges_3d):
|
||||
side_face_type = max(
|
||||
(face_type for face_type in face_edges_3d if face_type in (2, 3)),
|
||||
key=lambda face_type: face_scores.get(face_type, 0.0),
|
||||
)
|
||||
return edge_points_to_yaw(face_edges_3d[side_face_type], side_face_type)
|
||||
|
||||
if partial_points_3d is not None:
|
||||
face_edges_3d[partial_edge.face_type] = partial_points_3d
|
||||
face_scores[partial_edge.face_type] = max(face_scores.get(partial_edge.face_type, 0.0), 1.0)
|
||||
|
||||
if len(face_edges_3d) >= 2:
|
||||
yaw = visible_face_edges_to_yaw(face_edges_3d, face_scores=face_scores)
|
||||
if np.isfinite(yaw):
|
||||
return yaw
|
||||
|
||||
if fallback_face_type in range(4):
|
||||
return decode_visible_face_yaw_from_prediction(pred_41, pred_edge_60, anchor_xy, stride, fallback_face_type, calib)
|
||||
return visible_face_edges_to_yaw(face_edges_3d, face_scores=face_scores)
|
||||
|
||||
|
||||
def _back_project_metric_point(u, v, z, calib: ResizedCalib) -> np.ndarray:
|
||||
if calib is not None and z > 0:
|
||||
center_3d = back_project_2d_to_3d((u, v), z, calib)
|
||||
if center_3d is None:
|
||||
x3d, y3d = float("nan"), float("nan")
|
||||
else:
|
||||
x3d, y3d = center_3d[0], center_3d[1]
|
||||
else:
|
||||
x3d, y3d = float("nan"), float("nan")
|
||||
return np.array([x3d, y3d, z], dtype=np.float32)
|
||||
|
||||
|
||||
def extract_3d_attrs_from_prediction(
|
||||
pred_41,
|
||||
anchor_xy,
|
||||
stride,
|
||||
calib: ResizedCalib,
|
||||
face_type=None,
|
||||
pred_edge_60=None,
|
||||
) -> Optional[Prediction3DAttrs]:
|
||||
pred = pred_41
|
||||
rot_y = _decode_yaw_from_prediction(pred)
|
||||
dims = pred[27:30].astype(np.float32)
|
||||
|
||||
if face_type is None:
|
||||
z = float(pred[24])
|
||||
uv_offset = pred[25:27]
|
||||
edge_yaw = float("nan")
|
||||
else:
|
||||
off = FACE_OFFSETS_41[face_type]
|
||||
z = float(pred[off])
|
||||
uv_offset = pred[off + 1 : off + 3]
|
||||
edge_yaw = decode_multi_visible_face_yaw_from_prediction(
|
||||
pred,
|
||||
pred_edge_60,
|
||||
anchor_xy,
|
||||
stride,
|
||||
calib,
|
||||
fallback_face_type=face_type,
|
||||
)
|
||||
|
||||
u = float((anchor_xy[0] + uv_offset[0]) * stride)
|
||||
v = float((anchor_xy[1] + uv_offset[1]) * stride)
|
||||
center = _back_project_metric_point(u, v, z, calib)
|
||||
return Prediction3DAttrs(
|
||||
center=center,
|
||||
depth=z,
|
||||
dims=dims,
|
||||
yaw=float(rot_y),
|
||||
edge_yaw=float(edge_yaw),
|
||||
uv=np.array([u, v], dtype=np.float32),
|
||||
visible_face_type=None if face_type is None else int(face_type),
|
||||
face_center=None if face_type is None else center,
|
||||
)
|
||||
|
||||
|
||||
def face_center_from_corners(corners_3d, face_type):
|
||||
if corners_3d is None or face_type not in FACE_CORNERS:
|
||||
return None
|
||||
corners = np.asarray(corners_3d, dtype=np.float32)
|
||||
if corners.shape != (8, 3) or not np.isfinite(corners).all():
|
||||
return None
|
||||
return corners[list(FACE_CORNERS[face_type])].mean(axis=0)
|
||||
|
||||
|
||||
def rebuild_box_corners_for_visualization(
|
||||
corners_3d,
|
||||
dims,
|
||||
yaw,
|
||||
visible_face_type=None,
|
||||
face_center_3d=None,
|
||||
box_center_3d=None,
|
||||
):
|
||||
dims_arr = np.asarray(dims, dtype=np.float32)
|
||||
if dims_arr.shape != (3,) or not np.isfinite(dims_arr).all() or not np.isfinite(float(yaw)):
|
||||
return None
|
||||
|
||||
if visible_face_type is not None:
|
||||
if face_center_3d is None:
|
||||
face_center_3d = face_center_from_corners(corners_3d, int(visible_face_type))
|
||||
else:
|
||||
face_center_3d = np.asarray(face_center_3d, dtype=np.float32)
|
||||
if face_center_3d is None or face_center_3d.shape != (3,) or not np.isfinite(face_center_3d).all():
|
||||
return None
|
||||
return compute_3d_box_corners(face_center_3d, dims_arr, float(yaw), face_type=int(visible_face_type))
|
||||
|
||||
if box_center_3d is not None:
|
||||
box_center_3d = np.asarray(box_center_3d, dtype=np.float32)
|
||||
if box_center_3d.shape != (3,) or not np.isfinite(box_center_3d).all():
|
||||
return None
|
||||
return compute_3d_box_corners(box_center_3d, dims_arr, float(yaw), face_type=-1)
|
||||
|
||||
corners = np.asarray(corners_3d, dtype=np.float32)
|
||||
if corners.shape != (8, 3) or not np.isfinite(corners).all():
|
||||
return None
|
||||
return compute_3d_box_corners(corners.mean(axis=0), dims_arr, float(yaw), face_type=-1)
|
||||
1011
tools/model_inference/core/two_roi_infer_utils.py
Executable file
1011
tools/model_inference/core/two_roi_infer_utils.py
Executable file
File diff suppressed because it is too large
Load Diff
175
tools/model_inference/core/two_roi_types.py
Executable file
175
tools/model_inference/core/two_roi_types.py
Executable file
@@ -0,0 +1,175 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional, TypedDict
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
ColorBGR = tuple[int, int, int]
|
||||
ImagePoint2D = tuple[float, float]
|
||||
|
||||
|
||||
class RawCameraCalib(TypedDict, total=False):
|
||||
focal_u: float
|
||||
focal_v: float
|
||||
cu: float
|
||||
cv: float
|
||||
roll: float
|
||||
pitch: float
|
||||
yaw: float
|
||||
pos: list[float]
|
||||
distort_coeffs: list[float]
|
||||
image_width: Any
|
||||
image_height: Any
|
||||
source_format: str
|
||||
angle_unit: str
|
||||
|
||||
|
||||
class ROICropCalib(TypedDict):
|
||||
focal_u: float
|
||||
focal_v: float
|
||||
cu: float
|
||||
cv: float
|
||||
src_w: int
|
||||
src_h: int
|
||||
distort_coeffs: list[float]
|
||||
|
||||
|
||||
class ResizedCalib(TypedDict):
|
||||
fx: float
|
||||
fy: float
|
||||
cx: float
|
||||
cy: float
|
||||
distort_coeffs: list[float]
|
||||
depth_scale: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class DecodedVisibleEdge:
|
||||
face_type: int
|
||||
points_2d: np.ndarray
|
||||
depths: np.ndarray
|
||||
points_3d: Optional[np.ndarray] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Decoded3DPrediction:
|
||||
corners_3d: np.ndarray
|
||||
face_center_2d: Optional[ImagePoint2D]
|
||||
face_color: Optional[ColorBGR]
|
||||
visible_face_type: Optional[int]
|
||||
visible_face_types: tuple[int, ...]
|
||||
edge_points_2d: Optional[np.ndarray]
|
||||
edge_points_3d: Optional[np.ndarray]
|
||||
cls_id: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class Prediction3DAttrs:
|
||||
center: np.ndarray
|
||||
depth: float
|
||||
dims: np.ndarray
|
||||
yaw: float
|
||||
edge_yaw: float
|
||||
uv: np.ndarray
|
||||
visible_face_type: Optional[int]
|
||||
face_center: Optional[np.ndarray]
|
||||
|
||||
|
||||
class SerializedPredictionRecord(TypedDict, total=False):
|
||||
bbox_xyxy: Any
|
||||
confidence: float
|
||||
cls_id: int
|
||||
cls_name: str
|
||||
difficulty_logit: Optional[float]
|
||||
difficulty_prob: Optional[float]
|
||||
difficulty_label: Optional[int]
|
||||
difficulty_name: Optional[str]
|
||||
edge_head_available: bool
|
||||
xyzlhwyaw: Any
|
||||
xyzlhwyaw_ego: Any
|
||||
box_center_xyz: Any
|
||||
box_center_xyz_ego: Any
|
||||
depth_m: Optional[float]
|
||||
box_depth_m: Optional[float]
|
||||
lateral_distance_m: Optional[float]
|
||||
euclidean_distance_m: Optional[float]
|
||||
xz_distance_m: Optional[float]
|
||||
attribute: Any
|
||||
yaw_rad: Optional[float]
|
||||
edge_yaw_rad: Optional[float]
|
||||
edge_yaw_confident: bool
|
||||
edge_yaw_lateral_distance_m: Optional[float]
|
||||
edge_yaw_lateral_ok: bool
|
||||
edge_yaw_two_face_eligible: bool
|
||||
edge_yaw_selected_face_types: Any
|
||||
edge_yaw_selected_face_is_partial: Any
|
||||
edge_vs_reg_yaw_rad: Optional[float]
|
||||
selected_edge_direct_box_fit_available: bool
|
||||
selected_edge_direct_box_fit_mean_px: Optional[float]
|
||||
selected_edge_direct_box_fit_max_px: Optional[float]
|
||||
selected_edge_direct_box_fit_per_face_mean_px: Any
|
||||
selected_edge_edgeyaw_box_fit_available: bool
|
||||
selected_edge_edgeyaw_box_fit_mean_px: Optional[float]
|
||||
selected_edge_edgeyaw_box_fit_max_px: Optional[float]
|
||||
selected_edge_edgeyaw_box_fit_per_face_mean_px: Any
|
||||
selected_edge_fit_gain_px: Optional[float]
|
||||
edge_box_center_3d: Any
|
||||
edge_box_dims: Any
|
||||
edge_box_length_m: Optional[float]
|
||||
edge_box_width_m: Optional[float]
|
||||
edge_box_mode: Optional[str]
|
||||
edge_box_length_source: Optional[str]
|
||||
edge_box_width_source: Optional[str]
|
||||
all_edge_predictions: Any
|
||||
edge_selection: Any
|
||||
center_uv: Any
|
||||
center_3d: Any
|
||||
dims: Any
|
||||
cut_cls: Optional[int]
|
||||
roi_id: Optional[int]
|
||||
visible_face_type: Any
|
||||
visible_face_count: int
|
||||
visible_face_types: Any
|
||||
crop_bounds: list[int]
|
||||
original_bbox_xyxy: Any
|
||||
|
||||
|
||||
class SerializedROIPayload(TypedDict):
|
||||
crop_bounds: list[int]
|
||||
vp_x: float
|
||||
vp_y: float
|
||||
crop_center_x: float
|
||||
crop_center_y: float
|
||||
edge_head_available: bool
|
||||
edge_yaw_max_lateral_dist_m: float
|
||||
calib: dict[str, Any]
|
||||
predictions: list[SerializedPredictionRecord]
|
||||
|
||||
|
||||
class SerializedMergedPayload(TypedDict, total=False):
|
||||
method: str
|
||||
edge_head_available: bool
|
||||
roi_bounds: dict[str, list[int]]
|
||||
predictions: list[SerializedPredictionRecord]
|
||||
visualization: str
|
||||
|
||||
|
||||
class SerializedFramePayload(TypedDict, total=False):
|
||||
frame_index: int
|
||||
frame_name: str
|
||||
rois: dict[str, SerializedROIPayload]
|
||||
merged: SerializedMergedPayload
|
||||
merged_vru: SerializedMergedPayload
|
||||
visualization: str
|
||||
|
||||
|
||||
class SerializedPredictionsPayload(TypedDict):
|
||||
case_name: str
|
||||
images_dir: str
|
||||
calib_file: str
|
||||
exported_model_path: str
|
||||
edge_head_available: bool
|
||||
edge_yaw_max_lateral_dist_m: float
|
||||
frames: list[SerializedFramePayload]
|
||||
1
tools/model_inference/data_tools/__init__.py
Executable file
1
tools/model_inference/data_tools/__init__.py
Executable file
@@ -0,0 +1 @@
|
||||
"""Small data-preparation helpers for model_inference."""
|
||||
234
tools/model_inference/data_tools/extract_excel_column.py
Executable file
234
tools/model_inference/data_tools/extract_excel_column.py
Executable file
@@ -0,0 +1,234 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from openpyxl import load_workbook
|
||||
except ImportError:
|
||||
load_workbook = None
|
||||
|
||||
|
||||
FILE = Path(__file__).resolve()
|
||||
DEFAULT_INPUT_FILE = FILE.parents[1] / "examples" / "cncap" / "G1M3_AFS1616_CNCAP-2024_11月_0306.xlsx"
|
||||
DEFAULT_COLUMN_NAME = "原始数据地址"
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Extract one column from a CSV/XLSX table and print or save the values."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--input-file",
|
||||
type=str,
|
||||
default=str(DEFAULT_INPUT_FILE),
|
||||
help="Path to the input .xlsx or .csv file.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--column-name",
|
||||
type=str,
|
||||
default=DEFAULT_COLUMN_NAME,
|
||||
help="Header name of the target column.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sheet-name",
|
||||
type=str,
|
||||
default="",
|
||||
help="Worksheet name for .xlsx files. Defaults to the first sheet.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-file",
|
||||
type=str,
|
||||
default="",
|
||||
help="Optional output text file. If omitted, values are written to stdout.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--json-file",
|
||||
type=str,
|
||||
default="",
|
||||
help="Optional output json file. Defaults to a sibling json file next to the input table.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dedupe",
|
||||
action="store_true",
|
||||
help="Remove duplicate values while preserving the original order.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--list-columns",
|
||||
action="store_true",
|
||||
help="List the discovered header names and exit.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def normalize_header(value: str) -> str:
|
||||
return re.sub(r"\s+", "", str(value or "")).strip()
|
||||
|
||||
|
||||
def sanitize_filename(value: str) -> str:
|
||||
sanitized = re.sub(r'[\\/:*?"<>|]+', "_", str(value or "").strip())
|
||||
sanitized = re.sub(r"\s+", "_", sanitized)
|
||||
return sanitized.strip("._") or "column"
|
||||
|
||||
|
||||
def build_default_json_path(input_path: Path, column_name: str) -> Path:
|
||||
return input_path.with_name(f"{input_path.stem}_{sanitize_filename(column_name)}.json")
|
||||
|
||||
|
||||
def load_xlsx_table(path: Path, sheet_name: str) -> tuple[list[str], list[dict[str, str]], str]:
|
||||
if load_workbook is None:
|
||||
raise ImportError("openpyxl is required for .xlsx files. Please install it first.")
|
||||
|
||||
workbook = load_workbook(path, read_only=True, data_only=True)
|
||||
try:
|
||||
if sheet_name:
|
||||
if sheet_name not in workbook.sheetnames:
|
||||
available = ", ".join(workbook.sheetnames)
|
||||
raise ValueError(f"Worksheet {sheet_name!r} not found. Available sheets: {available}")
|
||||
worksheet = workbook[sheet_name]
|
||||
else:
|
||||
worksheet = workbook[workbook.sheetnames[0]]
|
||||
|
||||
header_row_values: list[str] | None = None
|
||||
header_indices: list[int] = []
|
||||
records: list[dict[str, str]] = []
|
||||
|
||||
for row in worksheet.iter_rows(values_only=True):
|
||||
normalized_row = [str(value).strip() if value is not None else "" for value in row]
|
||||
if not any(normalized_row):
|
||||
continue
|
||||
|
||||
if header_row_values is None:
|
||||
header_indices = [index for index, value in enumerate(normalized_row) if value]
|
||||
header_row_values = [normalized_row[index] for index in header_indices]
|
||||
continue
|
||||
|
||||
record = {
|
||||
header_row_values[index]: normalized_row[col_idx] if col_idx < len(normalized_row) else ""
|
||||
for index, col_idx in enumerate(header_indices)
|
||||
}
|
||||
if any(record.values()):
|
||||
records.append(record)
|
||||
|
||||
if header_row_values is None:
|
||||
raise ValueError("The worksheet is empty.")
|
||||
|
||||
return header_row_values, records, worksheet.title
|
||||
finally:
|
||||
workbook.close()
|
||||
|
||||
|
||||
def load_csv_table(path: Path) -> tuple[list[str], list[dict[str, str]], str]:
|
||||
with path.open("r", encoding="utf-8-sig", newline="") as file:
|
||||
sample = file.read(4096)
|
||||
file.seek(0)
|
||||
dialect = csv.Sniffer().sniff(sample) if sample.strip() else csv.excel
|
||||
reader = csv.DictReader(file, dialect=dialect)
|
||||
headers = reader.fieldnames or []
|
||||
records = []
|
||||
for row in reader:
|
||||
normalized_row = {str(key).strip(): str(value or "").strip() for key, value in row.items() if key is not None}
|
||||
if any(normalized_row.values()):
|
||||
records.append(normalized_row)
|
||||
return [str(header).strip() for header in headers], records, ""
|
||||
|
||||
|
||||
def load_table(path: Path, sheet_name: str) -> tuple[list[str], list[dict[str, str]], str]:
|
||||
suffix = path.suffix.lower()
|
||||
if suffix == ".xlsx":
|
||||
return load_xlsx_table(path, sheet_name)
|
||||
if suffix == ".csv":
|
||||
return load_csv_table(path)
|
||||
raise ValueError(f"Unsupported input format: {suffix}. Only .xlsx and .csv are supported.")
|
||||
|
||||
|
||||
def extract_column(records: list[dict[str, str]], column_name: str, dedupe: bool) -> list[str]:
|
||||
if not records:
|
||||
return []
|
||||
|
||||
normalized_to_actual = {normalize_header(name): name for name in records[0].keys()}
|
||||
target_key = normalized_to_actual.get(normalize_header(column_name))
|
||||
if target_key is None:
|
||||
available = ", ".join(records[0].keys())
|
||||
raise ValueError(f"Column {column_name!r} not found. Available columns: {available}")
|
||||
|
||||
values = [record.get(target_key, "").strip() for record in records]
|
||||
values = [value for value in values if value]
|
||||
if not dedupe:
|
||||
return values
|
||||
|
||||
deduped_values: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for value in values:
|
||||
if value in seen:
|
||||
continue
|
||||
seen.add(value)
|
||||
deduped_values.append(value)
|
||||
return deduped_values
|
||||
|
||||
|
||||
def write_output(values: list[str], output_file: str) -> None:
|
||||
content = "\n".join(values)
|
||||
if output_file:
|
||||
output_path = Path(output_file)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_text(content + ("\n" if values else ""), encoding="utf-8")
|
||||
print(f"Saved {len(values)} rows to {output_path}", file=sys.stderr)
|
||||
return
|
||||
|
||||
if content:
|
||||
sys.stdout.write(content)
|
||||
sys.stdout.write("\n")
|
||||
|
||||
|
||||
def write_json_output(
|
||||
input_path: Path,
|
||||
resolved_sheet_name: str,
|
||||
column_name: str,
|
||||
values: list[str],
|
||||
json_file: str,
|
||||
) -> Path:
|
||||
json_path = Path(json_file) if json_file else build_default_json_path(input_path, column_name)
|
||||
json_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
payload = {
|
||||
"input_file": str(input_path),
|
||||
"sheet_name": resolved_sheet_name,
|
||||
"column_name": column_name,
|
||||
"num_rows": len(values),
|
||||
"values": values,
|
||||
}
|
||||
json_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"Saved json to {json_path}", file=sys.stderr)
|
||||
return json_path
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
input_path = Path(args.input_file)
|
||||
if not input_path.is_file():
|
||||
raise FileNotFoundError(f"Input file not found: {input_path}")
|
||||
|
||||
headers, records, resolved_sheet_name = load_table(input_path, args.sheet_name)
|
||||
if args.list_columns:
|
||||
for header in headers:
|
||||
print(header)
|
||||
return 0
|
||||
|
||||
values = extract_column(records, args.column_name, args.dedupe)
|
||||
write_output(values, args.output_file)
|
||||
json_path = write_json_output(input_path, resolved_sheet_name, args.column_name, values, args.json_file)
|
||||
|
||||
sheet_info = f", sheet={resolved_sheet_name}" if resolved_sheet_name else ""
|
||||
print(
|
||||
f"Extracted {len(values)} rows from column {args.column_name!r} in {input_path}{sheet_info}, json={json_path}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
131
tools/model_inference/data_tools/parse_scene_csv_to_json.py
Executable file
131
tools/model_inference/data_tools/parse_scene_csv_to_json.py
Executable file
@@ -0,0 +1,131 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
FILE = Path(__file__).resolve()
|
||||
DEFAULT_INPUT_FILE = FILE.parents[1] / "examples" / "events" / "G1Q3_场地评测数据集.csv"
|
||||
DEFAULT_SCENE_COLUMN = "scene"
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Parse a CSV table and save a scene-keyed JSON file."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--input-file",
|
||||
type=str,
|
||||
default=str(DEFAULT_INPUT_FILE),
|
||||
help="Path to the input CSV file.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--scene-column",
|
||||
type=str,
|
||||
default=DEFAULT_SCENE_COLUMN,
|
||||
help="Column name used as the top-level JSON key.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-file",
|
||||
type=str,
|
||||
default="",
|
||||
help="Optional output JSON path. Defaults to a sibling .json file next to the CSV.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--keep-scene-field",
|
||||
action="store_true",
|
||||
help="Keep the scene column inside each grouped record.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def normalize_row(row: dict[str, str | None]) -> dict[str, str]:
|
||||
return {str(key).strip(): str(value or "").strip() for key, value in row.items() if key is not None}
|
||||
|
||||
|
||||
def resolve_output_path(input_path: Path, output_file: str) -> Path:
|
||||
if output_file:
|
||||
return Path(output_file)
|
||||
return input_path.with_suffix(".json")
|
||||
|
||||
|
||||
def load_csv_rows(path: Path) -> tuple[list[str], list[dict[str, str]]]:
|
||||
with path.open("r", encoding="utf-8-sig", newline="") as file:
|
||||
sample = file.read(4096)
|
||||
file.seek(0)
|
||||
dialect = csv.Sniffer().sniff(sample) if sample.strip() else csv.excel
|
||||
reader = csv.DictReader(file, dialect=dialect)
|
||||
headers = [str(header).strip() for header in (reader.fieldnames or [])]
|
||||
rows: list[dict[str, str]] = []
|
||||
for row in reader:
|
||||
normalized = normalize_row(row)
|
||||
if any(normalized.values()):
|
||||
rows.append(normalized)
|
||||
return headers, rows
|
||||
|
||||
|
||||
def group_rows_by_scene(
|
||||
rows: list[dict[str, str]],
|
||||
scene_column: str,
|
||||
keep_scene_field: bool,
|
||||
) -> OrderedDict[str, list[dict[str, str]]]:
|
||||
grouped: OrderedDict[str, list[dict[str, str]]] = OrderedDict()
|
||||
missing_scene_rows = 0
|
||||
|
||||
for row in rows:
|
||||
scene_value = row.get(scene_column, "").strip()
|
||||
if not scene_value:
|
||||
missing_scene_rows += 1
|
||||
continue
|
||||
|
||||
payload = dict(row)
|
||||
if not keep_scene_field:
|
||||
payload.pop(scene_column, None)
|
||||
|
||||
grouped.setdefault(scene_value, []).append(payload)
|
||||
|
||||
if missing_scene_rows > 0:
|
||||
print(
|
||||
f"Skipped {missing_scene_rows} rows because column {scene_column!r} was empty.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
return grouped
|
||||
|
||||
|
||||
def save_grouped_json(grouped: OrderedDict[str, list[dict[str, str]]], output_path: Path) -> None:
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_text(
|
||||
json.dumps(grouped, ensure_ascii=False, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
input_path = Path(args.input_file)
|
||||
if not input_path.is_file():
|
||||
raise FileNotFoundError(f"Input file not found: {input_path}")
|
||||
|
||||
headers, rows = load_csv_rows(input_path)
|
||||
if args.scene_column not in headers:
|
||||
available = ", ".join(headers)
|
||||
raise ValueError(f"Column {args.scene_column!r} not found. Available columns: {available}")
|
||||
|
||||
grouped = group_rows_by_scene(rows, args.scene_column, args.keep_scene_field)
|
||||
output_path = resolve_output_path(input_path, args.output_file)
|
||||
save_grouped_json(grouped, output_path)
|
||||
|
||||
print(
|
||||
f"Saved {len(rows)} rows across {len(grouped)} scenes to {output_path}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
48
tools/model_inference/docs/json_format.json
Executable file
48
tools/model_inference/docs/json_format.json
Executable file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"0": {
|
||||
"type": "0",
|
||||
"type_name": "truck",
|
||||
"score": "0.9664586186408997",
|
||||
"roi_id": "0",
|
||||
"box2d": [
|
||||
"1357.25390625",
|
||||
"542.76123046875",
|
||||
"1743.7847290039062",
|
||||
"803.9830627441406"
|
||||
],
|
||||
"xyzlhwyaw": [
|
||||
"3.5862553000515422",
|
||||
"0.6910880269131991",
|
||||
"5.21181583404541",
|
||||
"4.622976303100586",
|
||||
"1.9204307794570923",
|
||||
"1.481898546218872",
|
||||
"-1.5685003995895386"
|
||||
],
|
||||
"face_cls": "tail",
|
||||
"cut_cls": "0"
|
||||
},
|
||||
"1": {
|
||||
"type": "0",
|
||||
"type_name": "truck",
|
||||
"score": "0.9642692804336548",
|
||||
"roi_id": "0",
|
||||
"box2d": [
|
||||
"791.8895263671875",
|
||||
"498.2795104980469",
|
||||
"1162.1598205566406",
|
||||
"816.8395385742188"
|
||||
],
|
||||
"xyzlhwyaw": [
|
||||
"-0.04261477524786694",
|
||||
"0.5091491993856199",
|
||||
"6.116147041320801",
|
||||
"4.321102619171143",
|
||||
"2.0373644828796387",
|
||||
"1.6876970529556274",
|
||||
"-1.5730922222137451"
|
||||
],
|
||||
"face_cls": "tail",
|
||||
"cut_cls": "0"
|
||||
}
|
||||
}
|
||||
9
tools/model_inference/docs/model_inference_cncap2024.md
Executable file
9
tools/model_inference/docs/model_inference_cncap2024.md
Executable file
@@ -0,0 +1,9 @@
|
||||
tools/model_inference/examples/cncap/G1M3_AFS1616_CNCAP-202411.json文件中记录了一批场地评测数据的路径。
|
||||
|
||||
一组数据路径的示例为:"/mnt/hfs/project-G1M3/gt_org_data/G1M3_AFS1616/CNCAP2024数采/20251115/CVE/CPLA_RL_AEB_20_5_1_20251115140740/sigmastar.1",查找本地挂载路径时,需替换'hfs/project-G1M3'为'G1M3',同时查找路径下的camera4.bin。
|
||||
|
||||
具体的视频路径为"/mnt/hfs/project-G1M3/gt_org_data/G1M3_AFS1616/CNCAP2024数采/20251115/CVE/CPLA_RL_AEB_20_5_1_20251115140740/sigmastar.1/camera4.bin"
|
||||
|
||||
另外,其对应的标定参数路径为:"/mnt/hfs/project-G1M3/gt_org_data/G1M3_AFS1616/CNCAP2024数采/20251115/CVE/test_data/calibs/camera4.json"
|
||||
|
||||
现在期望tools/model_inference/run_two_roi_exported_onnx_infer.py脚本能够接收tools/model_inference/examples/cncap/G1M3_AFS1616_CNCAP-202411.json的输入,从而对相关的视频数据进行模型推理。
|
||||
5
tools/model_inference/docs/model_inference_through_eventid.md
Executable file
5
tools/model_inference/docs/model_inference_through_eventid.md
Executable file
@@ -0,0 +1,5 @@
|
||||
tools/model_inference/examples/events/G1Q3_场地评测数据集.json文件中记录了不同工况的数据,每个工况下有多组数据,每组数据中的data_path为eventid信息。
|
||||
|
||||
tools/model_inference/adapters/get_clip_by_eventid.py脚本可以用过event_id信息获取clip数据
|
||||
|
||||
现在期望tools/model_inference/run_two_roi_exported_onnx_infer.py脚本能够接收tools/model_inference/examples/events/G1Q3_场地评测数据集.json的输入,从而对相关的clip数据进行模型推理。
|
||||
8
tools/model_inference/docs/model_inference_through_video_dir.md
Executable file
8
tools/model_inference/docs/model_inference_through_video_dir.md
Executable file
@@ -0,0 +1,8 @@
|
||||
/data1/dongying/Mono3d/G1M3/test_data/cncap_20260408
|
||||
|
||||
上述路径中存放了多组测试数据,每组测试数据中有视频数据和标定参数,具体示例路径如下:
|
||||
|
||||
/data1/dongying/Mono3d/G1M3/test_data/cncap_20260408/20260407203248/sigmastar.1/camera4.bin
|
||||
/data1/dongying/Mono3d/G1M3/test_data/cncap_20260408/20260407203248/test_data/calibs/camera4.json
|
||||
|
||||
请对脚本tools/model_inference/run_two_roi_exported_onnx_infer.py的输入接口进行扩展,支持这种视频数据路径的模型推理。
|
||||
654
tools/model_inference/docs/two_roi_model_design.md
Executable file
654
tools/model_inference/docs/two_roi_model_design.md
Executable file
@@ -0,0 +1,654 @@
|
||||
# 双ROI导出模型设计说明(新版)
|
||||
|
||||
> 若当前使用的是带 `difficulty` 分支或 `fake 3D` 分支的新版本双 ROI 合并模型,请优先参考:
|
||||
> `tools/model_inference/docs/two_roi_model_design_with_diff_and_fake3d.md`
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
本文档以 `tools/model_inference/run_two_roi_exported_onnx_infer.py` 和 `tools/model_inference/core/run_two_roi_exported_onnx_infer.py` 的当前实现为准,说明新版双 ROI 导出模型在部署侧的输入约定、前处理、输出张量、后处理、结果序列化方式,以及与旧版说明文档之间的差异。
|
||||
|
||||
本文档关注的是“部署可执行设计”而不是训练侧的完整网络细节。凡与脚本实现冲突之处,以当前推理脚本和其依赖的本地工具模块为准。
|
||||
|
||||
如需查看旧版文档,可参考 `tools/model_inference/docs/two_roi_model_design_bak.md`。
|
||||
|
||||
---
|
||||
|
||||
## 2. 方案概览
|
||||
|
||||
新版方案仍然采用双 ROI 单目 3D 检测思路,但部署形态已经收敛为一个“自包含的合并导出模型 + Python 侧解码”的运行时:
|
||||
|
||||
1. 同一帧原图按两套 ROI 规则分别裁剪,得到 `ROI0` 和 `ROI1`。
|
||||
2. 两个 ROI 图像分别送入一个合并后的导出模型,模型输出每个 ROI 的原始检测头张量。
|
||||
3. 2D 框解码、Top-K 选择、3D 反归一化、深度恢复、3D 框重建、edge yaw 精化、可视化和 JSON 序列化全部在 Python 侧完成。
|
||||
|
||||
当前实现支持:
|
||||
|
||||
- `ONNX` 推理:通过 `onnxruntime`
|
||||
- `TorchScript` 推理:通过 `torch.jit.load`
|
||||
- 单 case 推理:`--case-dir`
|
||||
- mined eval 数据集批量推理:`--eval-dir`
|
||||
|
||||
当前运行时不依赖 `ultralytics`。
|
||||
|
||||
---
|
||||
|
||||
## 3. 模型与运行时契约
|
||||
|
||||
### 3.1 导出模型形态
|
||||
|
||||
当前脚本只接受双 ROI 合并后的导出模型:
|
||||
|
||||
- 后缀为 `.onnx` 时走 ONNX Runtime
|
||||
- 后缀为 `.torchscript` / `.ts` / `.jit` 时走 TorchScript
|
||||
|
||||
脚本会读取导出清单:
|
||||
|
||||
- ONNX 优先读取同名 sidecar:`merged_model.export.json`
|
||||
- TorchScript 优先读 sidecar,sidecar 不存在时再读模型内嵌的 `config.txt`
|
||||
|
||||
当前实现会显式校验:
|
||||
|
||||
```text
|
||||
export_mode == "raw_head_outputs"
|
||||
```
|
||||
|
||||
也就是说,当前部署脚本只支持“原始检测头输出”模式,不接受 `hybrid_outputs`、`postprocessed_outputs` 等其他导出模式。
|
||||
|
||||
### 3.2 输入输出名称
|
||||
|
||||
若导出清单可用,则直接使用清单中的:
|
||||
|
||||
- `input_names`
|
||||
- `output_names`
|
||||
- `input_sizes_wh`
|
||||
|
||||
若清单缺失,则回退到默认约定:
|
||||
|
||||
- 输入名:`roi0_input`、`roi1_input`
|
||||
- 输出名:
|
||||
- `roi0_boxes_head_raw`
|
||||
- `roi0_scores_head_raw`
|
||||
- `roi0_preds_3d_head_raw`
|
||||
- `roi0_preds_edge_head_raw`
|
||||
- `roi1_boxes_head_raw`
|
||||
- `roi1_scores_head_raw`
|
||||
- `roi1_preds_3d_head_raw`
|
||||
- `roi1_preds_edge_head_raw`
|
||||
|
||||
### 3.3 默认 ROI 配置
|
||||
|
||||
默认 ROI 配置来自 `tools/model_inference/core/two_roi_infer_utils.py` 中的 `DEFAULT_DATASET_CONFIG`:
|
||||
|
||||
| ROI | 裁剪尺寸 `(w, h)` | 裁剪中心模式 | `virtual_fx` | 默认输入尺寸 `(w, h)` |
|
||||
|---|---:|---|---:|---:|
|
||||
| `ROI0` | `1920 x 880` | `cxvy` | `537.0` | `768 x 352` |
|
||||
| `ROI1` | `768 x 352` | `vxvy` | `537.0` | `768 x 352` |
|
||||
|
||||
其中:
|
||||
|
||||
- `cxvy`:裁剪中心 `x` 取原图水平中心,`y` 取灭点 `vp_y`
|
||||
- `vxvy`:裁剪中心 `x` 取灭点 `vp_x`,`y` 取灭点 `vp_y`
|
||||
|
||||
这些默认值可以被 `--data-config`、`--roi0-data`、`--roi1-data` 中的 `roi_configs` 覆盖;若命令行显式传参,也可以进一步覆盖。
|
||||
|
||||
### 3.4 运行时元数据
|
||||
|
||||
推理脚本会从数据配置中加载以下元数据:
|
||||
|
||||
- `class_map`
|
||||
- `face_3d_classes`
|
||||
- `complete_3d_classes`
|
||||
- `norm_scales_3d`
|
||||
|
||||
若没有额外 YAML,使用内置默认值。
|
||||
|
||||
---
|
||||
|
||||
## 4. 前处理设计
|
||||
|
||||
### 4.1 相机标定读取
|
||||
|
||||
当前脚本兼容两种 `camera4.json` 结构:
|
||||
|
||||
1. 扁平格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"focal_u": ...,
|
||||
"focal_v": ...,
|
||||
"cu": ...,
|
||||
"cv": ...,
|
||||
"pitch": ...,
|
||||
"yaw": ...,
|
||||
"distort_coeffs": [...]
|
||||
}
|
||||
```
|
||||
|
||||
2. 合并格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"intrinsics": { "camera4.json": { ... } },
|
||||
"extrinsics": { "camera4.json": { "rpy": [...] } }
|
||||
}
|
||||
```
|
||||
|
||||
脚本会规范化出:
|
||||
|
||||
- `focal_u`, `focal_v`
|
||||
- `cu`, `cv`
|
||||
- `pitch`, `yaw`
|
||||
- `distort_coeffs`
|
||||
- `angle_unit`
|
||||
|
||||
### 4.2 灭点计算
|
||||
|
||||
灭点计算逻辑保持不变:
|
||||
|
||||
```text
|
||||
vp_x = cu + focal_u * tan(yaw)
|
||||
vp_y = cv - focal_v * tan(pitch)
|
||||
```
|
||||
|
||||
其中角度会先根据 `angle_unit` 统一到弧度制。
|
||||
|
||||
### 4.3 ROI 裁剪
|
||||
|
||||
对每一帧原图:
|
||||
|
||||
1. 读取原始宽高 `ori_w`, `ori_h`
|
||||
2. 根据 ROI 配置确定目标裁剪尺寸 `roi_w`, `roi_h`
|
||||
3. 根据 `crop_center_mode` 计算裁剪中心
|
||||
4. 通过 `compute_centered_roi_bounds()` 做边界裁剪
|
||||
|
||||
裁剪边界为:
|
||||
|
||||
```text
|
||||
crop_x1 = clamp(center_x - roi_w / 2, 0, ori_w - roi_w)
|
||||
crop_y1 = clamp(center_y - roi_h / 2, 0, ori_h - roi_h)
|
||||
crop_x2 = crop_x1 + roi_w
|
||||
crop_y2 = crop_y1 + roi_h
|
||||
```
|
||||
|
||||
### 4.4 缩放与标定更新
|
||||
|
||||
这是新版实现中一个关键变化点。
|
||||
|
||||
旧版文档默认把 ROI 裁剪结果直接一次性 resize 到模型输入尺寸;而当前实现使用 `_resize_ground3d_image_in_steps()`,先重复做若干次 `0.5x` 下采样,再做最后一次 resize,以匹配 Ground3D 训练侧的图像缩放行为。
|
||||
|
||||
缩放完成后,相机标定会更新到 ROI-resized 坐标系:
|
||||
|
||||
```text
|
||||
scale_x = target_w / crop_w
|
||||
scale_y = target_h / crop_h
|
||||
|
||||
fx = focal_u * scale_x
|
||||
fy = focal_v * scale_y
|
||||
cx = (cu - crop_x1) * scale_x
|
||||
cy = (cv - crop_y1) * scale_y
|
||||
|
||||
depth_scale = fx / virtual_fx
|
||||
```
|
||||
|
||||
其中:
|
||||
|
||||
- `fx, fy, cx, cy` 是 resized ROI 空间下的标定
|
||||
- `depth_scale` 用于后处理阶段把网络预测深度恢复到真实焦距尺度
|
||||
|
||||
### 4.5 图像张量化
|
||||
|
||||
当前脚本的输入张量构造为:
|
||||
|
||||
```python
|
||||
image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
|
||||
array = image_rgb.transpose(2, 0, 1).astype(np.float32) / 256.0
|
||||
input = array[None, ...]
|
||||
```
|
||||
|
||||
注意这里是:
|
||||
|
||||
```text
|
||||
/ 256.0
|
||||
```
|
||||
|
||||
而不是旧文档中的 `/ 255.0`。
|
||||
|
||||
TorchScript 路径与 ONNX 路径共用同一份前处理逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 5. 输出张量设计
|
||||
|
||||
### 5.1 每个 ROI 的输出分支
|
||||
|
||||
当前部署脚本假设每个 ROI 仍然输出四个分支:
|
||||
|
||||
1. `boxes_head_raw`
|
||||
2. `scores_head_raw`
|
||||
3. `preds_3d_head_raw`
|
||||
4. `preds_edge_head_raw`
|
||||
|
||||
即合并模型总共有 8 个输出张量。
|
||||
|
||||
### 5.2 3D 分支定义(41 维)
|
||||
|
||||
3D 分支通道布局沿用旧版设计:
|
||||
|
||||
| 通道范围 | 含义 |
|
||||
|---|---|
|
||||
| `0-5` | front face:`z3d, u_offset, v_offset, h, w, visible_score` |
|
||||
| `6-11` | rear face:`z3d, u_offset, v_offset, h, w, visible_score` |
|
||||
| `12-17` | left face:`z3d, u_offset, v_offset, l, h, visible_score` |
|
||||
| `18-23` | right face:`z3d, u_offset, v_offset, l, h, visible_score` |
|
||||
| `24` | whole-box `z3d` |
|
||||
| `25-26` | whole-box `u_offset, v_offset` |
|
||||
| `27-29` | whole-box `l, h, w` |
|
||||
| `30-33` | yaw 4-bin 分类 logits |
|
||||
| `34-37` | yaw 残差 `sin(delta)` |
|
||||
| `38-40` | cut state logits |
|
||||
|
||||
### 5.3 Edge 分支定义(60 维)
|
||||
|
||||
Edge 分支依然是四个面的底边缘采样点,每个采样点 3 维:
|
||||
|
||||
```text
|
||||
[du, dv, z]
|
||||
```
|
||||
|
||||
每个面 5 个点,共 `5 x 3 = 15` 维,四个面共 60 维:
|
||||
|
||||
| 面 | 通道范围 |
|
||||
|---|---|
|
||||
| front | `0-14` |
|
||||
| rear | `15-29` |
|
||||
| left | `30-44` |
|
||||
| right | `45-59` |
|
||||
|
||||
### 5.4 默认输出形状
|
||||
|
||||
对默认输入尺寸 `768 x 352`:
|
||||
|
||||
```text
|
||||
A = (352/8 * 768/8) + (352/16 * 768/16) + (352/32 * 768/32)
|
||||
= 4224 + 1056 + 264
|
||||
= 5544
|
||||
```
|
||||
|
||||
因此典型输出为:
|
||||
|
||||
| 输出名 | 形状 | 含义 |
|
||||
|---|---|---|
|
||||
| `roi*_boxes_head_raw` | `[1, 4 * reg_max, 5544]` | 2D 框原始回归输出 |
|
||||
| `roi*_scores_head_raw` | `[1, nc, 5544]` | 分类原始 logits |
|
||||
| `roi*_preds_3d_head_raw` | `[1, 41, 5544]` | 3D 原始预测 |
|
||||
| `roi*_preds_edge_head_raw` | `[1, 60, 5544]` | edge 原始预测 |
|
||||
|
||||
脚本支持 `reg_max > 1` 的通用 DFL 解码;当前导出模型若 `reg_max == 1`,会退化为直接距离回归。
|
||||
|
||||
---
|
||||
|
||||
## 6. 后处理设计
|
||||
|
||||
### 6.1 2D 框解码
|
||||
|
||||
2D 框解码由 `decode_boxes_xyxy()` 完成:
|
||||
|
||||
1. 将 `boxes_head_raw` 还原为 `l, t, r, b`
|
||||
2. 与预先缓存的 anchor 网格中心做几何组合
|
||||
3. 再乘以 stride 还原到 ROI-resized 图像像素坐标
|
||||
|
||||
anchor 由 `build_anchor_cache()` 预生成,默认 stride 为:
|
||||
|
||||
```text
|
||||
(8, 16, 32)
|
||||
```
|
||||
|
||||
### 6.2 Top-K 选择
|
||||
|
||||
当前脚本没有做 NMS,而是严格遵循导出 raw head 的 one-to-one Top-K 路径:
|
||||
|
||||
1. 对分类 logits 做 sigmoid
|
||||
2. 每个 anchor 取最大类别分数
|
||||
3. 先按 anchor 最大分类分数做一轮 Top-K
|
||||
4. 再对 gather 后的 `(anchor, class)` 展平分数做第二轮 Top-K
|
||||
5. 生成 `detections = [x1, y1, x2, y2, conf, cls_id]`
|
||||
|
||||
### 6.3 3D / Edge 反归一化
|
||||
|
||||
`preds_3d_head_raw` 和 `preds_edge_head_raw` 会用 `norm_scales_3d` 做反归一化:
|
||||
|
||||
| 项目 | 反归一化方式 |
|
||||
|---|---|
|
||||
| `z3d` | `raw * z3d_scale + z3d_offset` |
|
||||
| `u/v offset` | `sigmoid(raw) * 16 - 8` |
|
||||
| `size` | `raw * size_scale + size_offset` |
|
||||
| yaw residual | `tanh(raw)` |
|
||||
| edge `z` | `raw * z3d_scale + z3d_offset` |
|
||||
| edge `du/dv` | `sigmoid(raw) * 16 - 8` |
|
||||
|
||||
默认值来自内置配置:
|
||||
|
||||
```text
|
||||
z3d_scale = 24.415
|
||||
z3d_offset = 39.937
|
||||
size_scale = 1.945
|
||||
size_offset = 3.780
|
||||
```
|
||||
|
||||
### 6.4 深度恢复
|
||||
|
||||
由于训练时使用 `virtual_fx`,部署时必须恢复真实焦距尺度:
|
||||
|
||||
```text
|
||||
preds_3d[:, (0, 6, 12, 18, 24)] *= depth_scale
|
||||
preds_edge[:, 2::3] *= depth_scale
|
||||
```
|
||||
|
||||
这里的 `depth_scale = fx / virtual_fx` 是按每个 ROI、每一帧动态计算的。
|
||||
|
||||
### 6.5 置信度与类别过滤
|
||||
|
||||
当前脚本在完成 Top-K 后,再通过 `filter_prediction_rows()` 做最终过滤:
|
||||
|
||||
- `conf >= roi.spec.conf`
|
||||
- 若给了 `--classes`,则再做类别白名单过滤
|
||||
- 最终数量再截断到 `max_det`
|
||||
|
||||
### 6.6 3D 框解码与重建
|
||||
|
||||
#### 6.6.1 面型 3D 类别
|
||||
|
||||
对 `face_3d_classes` 中的类别,当前实现使用“可见面驱动”的 3D 解码:
|
||||
|
||||
1. 根据 `pred_41` 中的 face visibility 选择可见面
|
||||
2. 根据最佳可见面读取 `z_face + uv_face_offset`
|
||||
3. 利用整体尺寸 `dims_whole` 和回归的 `yaw` 重建 3D 框
|
||||
4. 若 `pred_edge_60` 可用,则同时解码可见底边缘点
|
||||
5. 若目标处于 cut 状态,还会尝试解码 partial side edge,并把它补充进可见面集合
|
||||
|
||||
这部分比旧版文档中的描述更具体,已经显式纳入了:
|
||||
|
||||
- cut state 感知
|
||||
- partial edge 补边
|
||||
- 多可见面 edge 采样
|
||||
|
||||
#### 6.6.2 完整体 3D 类别
|
||||
|
||||
对 `complete_3d_classes` 中的类别,直接使用 whole-box 分支:
|
||||
|
||||
- `z_whole`
|
||||
- `uv_whole`
|
||||
- `dims_whole`
|
||||
- `yaw`
|
||||
|
||||
重建完整 3D 包围盒。
|
||||
|
||||
#### 6.6.3 其他类别
|
||||
|
||||
对不属于上述两类集合的类别,脚本通常不会生成可视化 3D 框,但仍会保留 whole-box 分支解码出的:
|
||||
|
||||
- `center_3d`
|
||||
- `dims`
|
||||
- `yaw_rad`
|
||||
|
||||
用于结果序列化。
|
||||
|
||||
### 6.7 Yaw 解码
|
||||
|
||||
Yaw 解码仍是 4-bin 分类 + 残差方式:
|
||||
|
||||
```text
|
||||
best_bin = argmax(pred[30:34])
|
||||
yaw = arcsin(clamp(pred[34 + best_bin], -1, 1)) + yaw_bin_offset[best_bin]
|
||||
```
|
||||
|
||||
### 6.8 Edge Yaw 与 Edge Box 重建
|
||||
|
||||
新版实现中,edge 分支不只是“可选 yaw 精化”,而是形成了一条完整的 edge-based 几何诊断链:
|
||||
|
||||
1. 从 `pred_edge_60` 解码选中的可见底边缘点
|
||||
2. 反投影得到 3D edge points
|
||||
3. 由 `decode_edge_yaw_selection_from_prediction()` 选择最可信的单面或双面组合
|
||||
4. 计算 `edge_yaw_rad`
|
||||
5. 判断横向距离是否满足 `max_lateral_dist_m`
|
||||
6. 基于选中的 edge 几何重建 `edge_box`
|
||||
7. 若重建可信,则生成 `decoded_edge_heading`
|
||||
|
||||
默认横向距离阈值为:
|
||||
|
||||
```text
|
||||
edge_yaw_max_lateral_dist_m = 5.0
|
||||
```
|
||||
|
||||
当前实现额外产出以下诊断信息:
|
||||
|
||||
- `edge_yaw_confident`
|
||||
- `edge_yaw_lateral_distance_m`
|
||||
- `edge_yaw_lateral_ok`
|
||||
- `edge_yaw_two_face_eligible`
|
||||
- `edge_yaw_selected_face_types`
|
||||
- `edge_yaw_selected_face_is_partial`
|
||||
- `edge_vs_reg_yaw_rad`
|
||||
- `edge_box_center_3d`
|
||||
- `edge_box_dims`
|
||||
- `edge_box_mode`
|
||||
- `edge_box_length_source`
|
||||
- `edge_box_width_source`
|
||||
- `selected_edge_direct_box_fit_*`
|
||||
- `selected_edge_edgeyaw_box_fit_*`
|
||||
- `selected_edge_fit_gain_px`
|
||||
|
||||
### 6.9 可视化
|
||||
|
||||
每个 ROI 会输出三张 panel:
|
||||
|
||||
1. `2D`:绘制 ROI-resized 坐标系中的检测框
|
||||
2. `3D`:绘制常规回归 yaw 的 3D 框
|
||||
3. `3D EdgeRecon (1+ face)`:只绘制 `edge_yaw_confident=True` 的 edge 重建结果
|
||||
|
||||
最终按 ROI 拼成一个总览网格图。
|
||||
|
||||
---
|
||||
|
||||
## 7. 输出数据设计
|
||||
|
||||
### 7.1 输出目录结构
|
||||
|
||||
单 case 推理时,输出目录下会生成:
|
||||
|
||||
```text
|
||||
output_dir/
|
||||
├── visualizations/
|
||||
│ ├── *.jpg
|
||||
├── predictions/
|
||||
│ ├── *.json
|
||||
└── predictions.json
|
||||
```
|
||||
|
||||
其中:
|
||||
|
||||
- `visualizations/*.jpg`:每帧一个可视化网格图
|
||||
- `predictions/*.json`:下游消费的简化逐帧结果
|
||||
- `predictions.json`:完整聚合结果
|
||||
|
||||
批量 eval 推理时,会在 `output_dir` 下保留与 `eval_dir` 相同的相对目录层级。
|
||||
|
||||
### 7.2 完整聚合结果 `predictions.json`
|
||||
|
||||
聚合结果顶层结构为:
|
||||
|
||||
```json
|
||||
{
|
||||
"case_name": "...",
|
||||
"images_dir": "...",
|
||||
"calib_file": "...",
|
||||
"exported_model_path": "...",
|
||||
"edge_yaw_max_lateral_dist_m": 5.0,
|
||||
"frames": [
|
||||
{
|
||||
"frame_index": 0,
|
||||
"frame_name": "000000.png",
|
||||
"visualization": ".../visualizations/000000.jpg",
|
||||
"rois": {
|
||||
"roi0": {
|
||||
"crop_bounds": [x1, y1, x2, y2],
|
||||
"vp_x": ...,
|
||||
"vp_y": ...,
|
||||
"crop_center_x": ...,
|
||||
"crop_center_y": ...,
|
||||
"edge_yaw_max_lateral_dist_m": 5.0,
|
||||
"calib": {
|
||||
"fx": ...,
|
||||
"fy": ...,
|
||||
"cx": ...,
|
||||
"cy": ...,
|
||||
"depth_scale": ...
|
||||
},
|
||||
"predictions": [
|
||||
{
|
||||
"bbox_xyxy": [...],
|
||||
"original_bbox_xyxy": [...],
|
||||
"confidence": 0.93,
|
||||
"cls_id": 6,
|
||||
"cls_name": "truck",
|
||||
"center_uv": [...],
|
||||
"center_3d": [...],
|
||||
"dims": [...],
|
||||
"yaw_rad": ...,
|
||||
"edge_yaw_rad": ...,
|
||||
"edge_yaw_confident": true,
|
||||
"cut_cls": 0,
|
||||
"roi_id": 0,
|
||||
"visible_face_type": 1,
|
||||
"visible_face_types": [1, 2]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 简化逐帧结果 `predictions/*.json`
|
||||
|
||||
脚本还会额外导出更接近下游消费格式的逐帧 JSON,例如:
|
||||
|
||||
```json
|
||||
{
|
||||
"0": {
|
||||
"type": "0",
|
||||
"type_name": "truck",
|
||||
"score": "0.9664",
|
||||
"roi_id": "0",
|
||||
"box2d": ["1357.25", "542.76", "1743.78", "803.98"],
|
||||
"xyzlhwyaw": ["3.58", "0.69", "5.21", "4.62", "1.92", "1.48", "-1.57"],
|
||||
"face_cls": "tail",
|
||||
"cut_cls": "0",
|
||||
"edge_yaw_rad": "-1.56",
|
||||
"edge_yaw_confident": true,
|
||||
"edge_vs_reg_yaw_rad": "0.01"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这里有一个新版语义变化:
|
||||
|
||||
- `box2d` 优先使用 `original_bbox_xyxy`
|
||||
- 若原图坐标不可用,才回退到 `bbox_xyxy`
|
||||
|
||||
也就是说,简化逐帧 JSON 里的 2D 框优先是“原图坐标系”。
|
||||
|
||||
### 7.4 坐标系约定
|
||||
|
||||
当前实现涉及四种常见坐标系:
|
||||
|
||||
1. `bbox_xyxy`
|
||||
ROI 裁剪并缩放后的图像坐标系
|
||||
|
||||
2. `original_bbox_xyxy`
|
||||
原始整图坐标系
|
||||
|
||||
3. `center_uv`
|
||||
ROI-resized 图像坐标系下的中心点
|
||||
|
||||
4. `center_3d`
|
||||
相机坐标系,单位米
|
||||
|
||||
旧版文档中“所有二维坐标均在 ROI 坐标系”这一表述对当前实现已不完全成立,因为现在明确同时输出了 `original_bbox_xyxy`。
|
||||
|
||||
---
|
||||
|
||||
## 8. 默认类别配置
|
||||
|
||||
当前运行时默认的 `class_map` 为:
|
||||
|
||||
| `cls_id` | 类别 |
|
||||
|---|---|
|
||||
| `0` | `car` |
|
||||
| `1` | `suv` |
|
||||
| `2` | `pickup` |
|
||||
| `3` | `medium_car` |
|
||||
| `4` | `van` |
|
||||
| `5` | `bus` |
|
||||
| `6` | `truck` / `tanker` / `large_truck` / `construction_vehicle` |
|
||||
| `7` | `special_vehicle` |
|
||||
| `8` | `unknown` |
|
||||
| `9` | `pedestrian` |
|
||||
| `10` | `bicyclist` / `motorcyclist` |
|
||||
| `11` | `bicycle` / `motorcycle` |
|
||||
| `12` | `tricycle` / `tricyclist` |
|
||||
| `13` | `traffic_sign` |
|
||||
| `14` | `wheel` |
|
||||
| `15` | `plate` |
|
||||
| `16` | `face` |
|
||||
|
||||
默认 3D 类别分组为:
|
||||
|
||||
- `face_3d_classes = {0,1,2,3,4,5,6,7,8}`
|
||||
- `complete_3d_classes = {9,10,11,12}`
|
||||
|
||||
因此:
|
||||
|
||||
- `0-8` 使用“可见面驱动”的 3D 解码
|
||||
- `9-12` 使用 whole-box 3D 解码
|
||||
- `13-16` 默认不绘制 3D 框,但仍保留部分序列化属性
|
||||
|
||||
---
|
||||
|
||||
## 9. 新旧版本差异总结
|
||||
|
||||
下表总结了新版实现相对旧版说明文档的主要差异。
|
||||
|
||||
| 项目 | 旧版说明 | 新版实现 |
|
||||
|---|---|---|
|
||||
| 导出模式 | 文档列出了 `raw_head_outputs`、`hybrid_outputs`、`postprocessed_outputs`、`denorm_branch_outputs` | 当前脚本只接受 `raw_head_outputs`,并在启动时校验 |
|
||||
| 后端支持 | 主要按 ONNX 方案描述 | 当前同时支持 `ONNX` 和 `TorchScript`,并自动解析 manifest |
|
||||
| 运行时依赖 | 更偏训练/导出视角 | 当前是完全自包含的部署侧实现,不依赖 `ultralytics` |
|
||||
| ROI 缩放 | 单次 resize 描述 | 当前实现先多次 `0.5x` 下采样,再最终 resize,以贴合训练前处理 |
|
||||
| 图像归一化 | `/255.0` | 当前脚本实际使用 `/256.0` |
|
||||
| 输入尺寸来源 | 假定固定输入尺寸 | 当前优先读 manifest 中的 `input_sizes_wh`,也支持命令行覆盖 |
|
||||
| 标定输入格式 | 主要描述平铺内参格式 | 当前同时兼容 flat `camera4.json` 和 combined calibration |
|
||||
| 类别定义 | 类别表较粗,`face_3d_classes` 和 `complete_3d_classes` 范围较小 | 默认类表扩展到 17 个 id,`face_3d_classes=0-8`,`complete_3d_classes=9-12` |
|
||||
| 3D 解码 | 描述了基础 face / whole 两条路径 | 当前新增 cut-aware partial edge、edge 几何筛选、edge box 重建等更完整逻辑 |
|
||||
| edge yaw | 作为可选精化模块描述 | 当前 edge yaw 已融入主结果结构,带 lateral gating、双面可见判断和拟合残差诊断 |
|
||||
| 输出 JSON | 主要描述聚合 JSON,字段较少 | 当前同时输出完整聚合 JSON 和简化逐帧 JSON,并新增大量 edge 诊断字段 |
|
||||
| 2D 坐标语义 | 默认都在 ROI-resized 坐标系 | 当前同时输出 `bbox_xyxy` 和 `original_bbox_xyxy`,两种坐标系并存 |
|
||||
| 可视化 | 只描述 2D/3D 结果 | 当前每个 ROI 输出 `2D`、`3D`、`3D EdgeRecon` 三个 panel |
|
||||
| 运行模式 | 以单 case 为主 | 当前新增 `--eval-dir`,支持整个 mined eval 数据集复用同一个模型批量跑 |
|
||||
|
||||
### 9.1 对齐建议
|
||||
|
||||
如果后续继续维护部署文档,建议把以下三项视为“新版真值来源”:
|
||||
|
||||
1. `tools/model_inference/run_two_roi_exported_onnx_infer.py`
|
||||
2. `tools/model_inference/core/two_roi_infer_utils.py`
|
||||
3. `tools/model_inference/core/two_roi_3d_utils.py`
|
||||
|
||||
特别是以下内容最容易因实现更新而与文档产生偏差:
|
||||
|
||||
- 输入归一化常数
|
||||
- ROI resize 策略
|
||||
- 类别映射与 3D 类别分组
|
||||
- edge yaw 的筛选和诊断字段
|
||||
- 简化 JSON 中 `box2d` 的坐标系定义
|
||||
385
tools/model_inference/docs/two_roi_model_design_bak.md
Executable file
385
tools/model_inference/docs/two_roi_model_design_bak.md
Executable file
@@ -0,0 +1,385 @@
|
||||
# 双ROI合并模型方案文档
|
||||
|
||||
## 1. 概述
|
||||
|
||||
本文档描述基于 YOLO26-3D 的双 ROI(Region of Interest)单目3D目标检测方案。该方案将同一张原始图像按两种不同的裁剪策略分别送入两个独立训练的 Detect3D 模型,并将两模型合并导出为单一 ONNX/TorchScript 文件,实现2D检测框与3D属性(深度、尺寸、朝向角)的联合预测。
|
||||
|
||||
---
|
||||
|
||||
## 2. 模型架构
|
||||
|
||||
### 2.1 骨干网络与检测头
|
||||
|
||||
模型基于 `yolo26-3d.yaml` 配置,结构如下:
|
||||
|
||||
```
|
||||
输入 (3 × H × W)
|
||||
├── Backbone: Conv → C3k2 × 2 → Conv → C3k2 × 2 → Conv → C3k2 → Conv → C3k2 → SPPF → C2PSA
|
||||
└── Neck (FPN): Upsample + Concat × 2 → C3k2
|
||||
→ Detect3D(P3/8, P4/16, P5/32)
|
||||
```
|
||||
|
||||
模型使用 `end2end=True`,即训练时启用 one-to-one 分支,推理时直接输出 Top-K 检测结果,无需 NMS。
|
||||
|
||||
### 2.2 Detect3D 检测头
|
||||
|
||||
`Detect3D` 继承自标准 `Detect`,在2D分支(cv2 box、cv3 cls)基础上新增:
|
||||
|
||||
- **cv4(3D预测分支)**:每个 anchor 输出 41 维张量;
|
||||
- **cv5(可见面边缘分支)**:每个 anchor 输出 60 维张量。
|
||||
|
||||
#### 3D预测张量(41维)格式
|
||||
|
||||
| 通道范围 | 含义 |
|
||||
|----------|------|
|
||||
| 0–5 | 前面(Front face): z3d, u_offset, v_offset, h, w, visible_score |
|
||||
| 6–11 | 后面(Rear face): z3d, u_offset, v_offset, h, w, visible_score |
|
||||
| 12–17 | 左面(Left face): z3d, u_offset, v_offset, l, h, visible_score |
|
||||
| 18–23 | 右面(Right face): z3d, u_offset, v_offset, l, h, visible_score |
|
||||
| 24 | 整体 z3d(米) |
|
||||
| 25–26 | 整体 u_offset, v_offset(网格坐标偏移) |
|
||||
| 27–29 | 整体 l, h, w(米) |
|
||||
| 30–33 | 4个朝向 bin 的分类 logits |
|
||||
| 34–37 | 4个朝向 bin 的残差 sin 值 |
|
||||
| 38–40 | cut 状态分类 logits(normal/cut-in/cut-out) |
|
||||
|
||||
#### 可见面边缘张量(60维)格式
|
||||
|
||||
每张可见面对应 5 个采样点 × 3 维(du, dv, z),共 4 个面 × 15 维 = 60 维:
|
||||
|
||||
| 面索引 | 通道范围 |
|
||||
|--------|----------|
|
||||
| 前面 | 0–14 |
|
||||
| 后面 | 15–29 |
|
||||
| 左面 | 30–44 |
|
||||
| 右面 | 45–59 |
|
||||
|
||||
每个采样点格式:`[du (网格偏移), dv (网格偏移), z (米)]`。
|
||||
|
||||
### 2.3 双 ROI 设计
|
||||
|
||||
两个独立模型分别对同一帧图像的不同感兴趣区域进行预测:
|
||||
|
||||
| 参数 | ROI0 | ROI1 |
|
||||
|------|------|------|
|
||||
| 裁剪尺寸(宽×高) | 1920 × 880 | 768 × 352 |
|
||||
| 裁剪中心模式 | `cxvy`(图像水平中心 + 灭点y) | `vxvy`(灭点x + 灭点y) |
|
||||
| 虚拟焦距 virtual_fx | 537.0 | 537.0 |
|
||||
| 模型输入尺寸 | 768 × 352 | 768 × 352 |
|
||||
|
||||
- **ROI0**:以图像水平中心为裁剪中心x,涵盖宽视野,适合远距离大视野检测;
|
||||
- **ROI1**:以灭点为裁剪中心,聚焦正前方,适合近中距离精细检测。
|
||||
|
||||
### 2.4 合并导出
|
||||
|
||||
两模型通过 `merge_models_of_2roi_yolo26.py` 合并为单一模型文件(`merged_model.onnx` 或 `.torchscript`),导出时同时写出 `merged_model.export.json` 元数据 sidecar 文件,记录 input_names、output_names、input_sizes_wh 等信息。
|
||||
|
||||
支持多种导出模式(`--export-mode`):
|
||||
|
||||
| 模式 | 描述 |
|
||||
|------|------|
|
||||
| `raw_head_outputs`(默认) | 输出各分支的原始未后处理张量,所有解码在推理侧完成 |
|
||||
| `hybrid_outputs` | 输出后处理2D检测 + 原始 3D/edge 张量 |
|
||||
| `postprocessed_outputs` | 输出完整后处理结果(含 Top-K 选取) |
|
||||
| `denorm_branch_outputs` | 输出反归一化后、Top-K 选取前的分支张量 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 推理前处理
|
||||
|
||||
### 3.1 加载相机标定
|
||||
|
||||
从 `camera4.json` 读取原始相机内参和外参:
|
||||
|
||||
```
|
||||
RawCameraCalib:
|
||||
focal_u, focal_v — 原始像素焦距
|
||||
cu, cv — 主点 (principal point)
|
||||
pitch, yaw — 相机安装角度(弧度)
|
||||
distort_coeffs — 鱼眼畸变系数 [k1, k2, k3, k4](可选)
|
||||
```
|
||||
|
||||
### 3.2 计算灭点(Vanishing Point)
|
||||
|
||||
根据相机安装姿态计算图像灭点坐标:
|
||||
|
||||
```
|
||||
vp_x = cu + focal_u × tan(yaw)
|
||||
vp_y = cv - focal_v × tan(pitch)
|
||||
```
|
||||
|
||||
### 3.3 ROI 裁剪
|
||||
|
||||
根据 ROI 规格(`roi_size`、`crop_center_mode`)在原图上确定裁剪区域:
|
||||
|
||||
```
|
||||
crop_center_x:
|
||||
cxvy 模式: ori_w / 2
|
||||
vxvy 模式: vp_x
|
||||
|
||||
crop_center_y = vp_y(两种模式均以灭点y为中心)
|
||||
|
||||
crop_bounds [x1, y1, x2, y2]:
|
||||
x1 = clamp(crop_center_x - roi_w/2, 0, ori_w - roi_w)
|
||||
y1 = clamp(crop_center_y - roi_h/2, 0, ori_h - roi_h)
|
||||
```
|
||||
|
||||
裁剪结果为固定尺寸图像块(roi_w × roi_h)。
|
||||
|
||||
### 3.4 缩放与标定更新
|
||||
|
||||
将裁剪图像缩放到模型输入尺寸(768 × 352),同时更新相机内参:
|
||||
|
||||
```
|
||||
scale_x = target_w / crop_w
|
||||
scale_y = target_h / crop_h
|
||||
|
||||
fx = focal_u × scale_x
|
||||
fy = focal_v × scale_y
|
||||
cx = (cu - crop_x1) × scale_x
|
||||
cy = (cv - crop_y1) × scale_y
|
||||
|
||||
depth_scale = fx / virtual_fx # 深度恢复系数
|
||||
```
|
||||
|
||||
`depth_scale` 记录了当前帧缩放后真实焦距与虚拟训练焦距之间的比值,用于后处理阶段的深度恢复。
|
||||
|
||||
### 3.5 图像归一化
|
||||
|
||||
```
|
||||
image_rgb = BGR → RGB 转换
|
||||
tensor = image_rgb.transpose(2,0,1) # HWC → CHW
|
||||
tensor = tensor / 255.0 # 归一化到 [0, 1]
|
||||
input = tensor[None, ...] # 增加 batch 维
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 模型输出格式
|
||||
|
||||
以默认 `raw_head_outputs` 模式为例,合并模型共输出 **8 个张量**(每 ROI 4 个):
|
||||
|
||||
| 输出名 | 形状 | 含义 |
|
||||
|--------|------|------|
|
||||
| `roi0_boxes_head_raw` | `[1, 4×reg_max, A]` | ROI0 DFL box 回归原始 logits |
|
||||
| `roi0_scores_head_raw` | `[1, nc, A]` | ROI0 类别分数原始 logits |
|
||||
| `roi0_preds_3d_head_raw` | `[1, 41, A]` | ROI0 3D 预测原始值 |
|
||||
| `roi0_preds_edge_head_raw` | `[1, 60, A]` | ROI0 可见面边缘原始值 |
|
||||
| `roi1_boxes_head_raw` | `[1, 4×reg_max, A]` | ROI1 DFL box 回归原始 logits |
|
||||
| `roi1_scores_head_raw` | `[1, nc, A]` | ROI1 类别分数原始 logits |
|
||||
| `roi1_preds_3d_head_raw` | `[1, 41, A]` | ROI1 3D 预测原始值 |
|
||||
| `roi1_preds_edge_head_raw` | `[1, 60, A]` | ROI1 可见面边缘原始值 |
|
||||
|
||||
其中 `nc` 为类别数,`A` 为总 anchor 数(三个尺度之和):
|
||||
|
||||
```
|
||||
A = (H/8 × W/8) + (H/16 × W/16) + (H/32 × W/32)
|
||||
对于 352×768: = (44×96) + (22×48) + (11×24) = 4224 + 1056 + 264 = 5544
|
||||
```
|
||||
|
||||
`reg_max=1`(当前配置),因此 box 分支通道数为 4。
|
||||
|
||||
---
|
||||
|
||||
## 5. 后处理步骤
|
||||
|
||||
### 5.1 2D 检测框解码
|
||||
|
||||
**DFL 解码**(分布式焦点损失解码):
|
||||
|
||||
```python
|
||||
# reg_max == 1 时简化为:
|
||||
dist = raw_boxes.reshape(batch, 4, A) # [l, t, r, b] 形式的距离预测
|
||||
|
||||
# 还原为 anchor-relative xyxy:
|
||||
boxes_xyxy[:, 0:2] = anchors - dist[:, 0:2] # x1, y1
|
||||
boxes_xyxy[:, 2:4] = anchors + dist[:, 2:4] # x2, y2
|
||||
|
||||
# 乘以 stride 还原到像素坐标:
|
||||
boxes_xyxy = boxes_xyxy * strides
|
||||
```
|
||||
|
||||
Anchor 坐标为各尺度特征图的网格中心点(0.5 偏移),由 `build_anchor_cache()` 预计算。
|
||||
|
||||
### 5.2 Top-K 选取(无 NMS)
|
||||
|
||||
模型使用 end-to-end one-to-one 训练,推理时直接按类别最高分取 Top-K(默认 K=300),无需 NMS:
|
||||
|
||||
```python
|
||||
# 1. sigmoid 激活类别分数
|
||||
scores = sigmoid(raw_scores) # [1, A, nc]
|
||||
|
||||
# 2. 每个 anchor 取最大分数,选 Top-K anchor
|
||||
anchor_max_scores = scores.max(axis=-1) # [1, A]
|
||||
topk_anchor_indices = argsort(anchor_max_scores)[-K:]
|
||||
|
||||
# 3. 再对 gathered scores 扁平化取 Top-K(双重 Top-K)
|
||||
# 最终输出形状: detections [1, K, 6] (x1,y1,x2,y2, conf, cls_id)
|
||||
```
|
||||
|
||||
### 5.3 3D 预测反归一化
|
||||
|
||||
原始 3D 张量在网络内经过归一化压缩,推理侧需还原为物理量:
|
||||
|
||||
| 通道 | 操作 | 含义 |
|
||||
|------|------|------|
|
||||
| z3d(ch 0,6,12,18,24) | `raw × z3d_scale + z3d_offset` | 深度(米),默认 scale=24.415, offset=39.937 |
|
||||
| UV 偏移(ch 1-2, 7-8, 13-14, 19-20, 25-26) | `sigmoid(raw) × 16 − 8` | 网格坐标偏移(格子数,范围约 [−8, 8]) |
|
||||
| 尺寸 l/h/w(ch 3-4, 9-10, 15-16, 21-22, 27-29) | `raw × size_scale + size_offset` | 尺寸(米),默认 scale=1.945, offset=3.780 |
|
||||
| 朝向 bin logits(ch 30-33) | 保持原始 logits | 4-bin softmax 分类 |
|
||||
| 朝向残差(ch 34-37) | `tanh(raw)` | sin(Δ) in [−1, 1] |
|
||||
| face visibility(ch 5,11,17,23) | 保持原始分数 | 各面可见置信度 |
|
||||
| cut 状态(ch 38-40) | 保持原始 logits | normal/cut-in/cut-out |
|
||||
|
||||
同理,edge 张量(60维)的 UV 偏移和深度 z 也做相同的 sigmoid/线性反归一化。
|
||||
|
||||
### 5.4 深度恢复(Depth Scale 还原)
|
||||
|
||||
由于训练时使用固定 `virtual_fx` 虚拟焦距,而实际推理焦距因裁剪+缩放而变化,需乘以 `depth_scale` 还原真实深度:
|
||||
|
||||
```python
|
||||
preds_3d[:, [0, 6, 12, 18, 24]] *= depth_scale # z3d 通道
|
||||
preds_edge[:, 2::3] *= depth_scale # edge 深度通道
|
||||
```
|
||||
|
||||
其中 `depth_scale = fx_actual / virtual_fx`,每帧根据实际裁剪/缩放参数计算。
|
||||
|
||||
### 5.5 置信度过滤
|
||||
|
||||
按置信度阈值(默认 0.25)和类别白名单过滤低质量检测:
|
||||
|
||||
```python
|
||||
keep = detections[:, 4] >= conf_thres
|
||||
if classes is not None:
|
||||
keep &= cls_id in classes
|
||||
detections = detections[keep][:max_det]
|
||||
```
|
||||
|
||||
### 5.6 朝向角解码
|
||||
|
||||
模型采用 **4-bin 分类 + 残差 sin** 联合解码朝向角:
|
||||
|
||||
```
|
||||
4 个 bin 偏置角: [0°, 90°, −90°, 180°]
|
||||
|
||||
yaw_bin = argmax(logits[30:34])
|
||||
bin_offset = {0: 0, 1: π/2, 2: −π/2, 3: π}[yaw_bin]
|
||||
|
||||
sin_delta = tanh(raw[34 + yaw_bin])
|
||||
delta = arcsin(clamp(sin_delta, −1, 1))
|
||||
|
||||
yaw_reg = bin_offset + delta
|
||||
```
|
||||
|
||||
### 5.7 3D 包围盒重建
|
||||
|
||||
#### 面型类(face_3d_classes):车辆等大目标
|
||||
|
||||
1. 按 face visibility score 选取最高置信可见面(front/rear/left/right);
|
||||
2. 用该面的 UV 偏移 + z 在像素坐标系中反投影,得到 3D 中心点;
|
||||
3. 结合整体尺寸 l/h/w 和朝向角 yaw,计算 8 个角点坐标;
|
||||
4. 若有 edge 预测,用 cv5 中对应面的5个采样点重投影,辅助可视化。
|
||||
|
||||
```python
|
||||
# 面中心 UV → 像素坐标
|
||||
u_face = (anchor_x + u_offset) * stride
|
||||
v_face = (anchor_y + v_offset) * stride
|
||||
|
||||
# 像素坐标 → 相机坐标系 3D 中心
|
||||
X = (u_face - cx) / fx * z_face
|
||||
Y = (v_face - cy) / fy * z_face
|
||||
Z = z_face
|
||||
|
||||
# 基于 3D 中心 + 尺寸 + 朝向重建 8 角点
|
||||
corners = compute_3d_box_corners(center_3d, [l, h, w], yaw, face_type)
|
||||
```
|
||||
|
||||
#### 完整型类(complete_3d_classes):行人、骑手等
|
||||
|
||||
直接使用整体预测(通道 24–29)重建包围盒,无需面选择。
|
||||
|
||||
### 5.8 Edge Yaw 精化(可选)
|
||||
|
||||
对于横向距离 ≤ `edge_yaw_max_lateral_dist_m`(默认 5.0m)的目标,尝试利用可见面底边缘点拟合更精确的朝向角:
|
||||
|
||||
1. 从 cv5 解码可选的1-2个可见面底边缘采样点(2D);
|
||||
2. 结合深度,将采样点反投影到 3D;
|
||||
3. 拟合底边缘方向,得到 `edge_yaw_rad`;
|
||||
4. 若两面同时可见(two-face eligible),优先使用双面拟合结果,置 `edge_yaw_confident=True`。
|
||||
|
||||
---
|
||||
|
||||
## 6. 输出数据格式
|
||||
|
||||
每帧的结构化输出以 JSON 格式保存:
|
||||
|
||||
```json
|
||||
{
|
||||
"frame_index": 0,
|
||||
"frame_name": "000000.png",
|
||||
"rois": {
|
||||
"roi0": {
|
||||
"crop_bounds": [x1, y1, x2, y2],
|
||||
"vp_x": 960.0,
|
||||
"vp_y": 175.0,
|
||||
"calib": { "fx": ..., "fy": ..., "cx": ..., "cy": ..., "depth_scale": ... },
|
||||
"predictions": [
|
||||
{
|
||||
"bbox_xyxy": [x1, y1, x2, y2],
|
||||
"confidence": 0.87,
|
||||
"cls_id": 0,
|
||||
"cls_name": "car",
|
||||
"yaw_rad": -0.32,
|
||||
"edge_yaw_rad": -0.31,
|
||||
"edge_yaw_confident": true,
|
||||
"edge_yaw_lateral_distance_m": 3.2,
|
||||
"edge_yaw_lateral_ok": true,
|
||||
"edge_yaw_two_face_eligible": true,
|
||||
"edge_yaw_selected_face_types": [2, 0],
|
||||
"edge_vs_reg_yaw_rad": 0.01,
|
||||
"center_uv": [u, v],
|
||||
"center_3d": [X, Y, Z],
|
||||
"visible_face_type": 2,
|
||||
"visible_face_types": [2, 0],
|
||||
"crop_bounds": [x1, y1, x2, y2]
|
||||
}
|
||||
]
|
||||
},
|
||||
"roi1": { ... }
|
||||
},
|
||||
"visualization": "/path/to/000000.jpg"
|
||||
}
|
||||
```
|
||||
|
||||
所有二维坐标(`bbox_xyxy`、`center_uv`)均在对应 ROI 裁剪并缩放后的图像坐标系下;`center_3d` 在相机坐标系下,单位为米。
|
||||
|
||||
---
|
||||
|
||||
## 7. 类别定义
|
||||
|
||||
| cls_id | 类别名称(示例) |
|
||||
|--------|-----------------|
|
||||
| 0 | car(含 suv, van 等) |
|
||||
| 1 | bus |
|
||||
| 2 | truck(含 large_truck) |
|
||||
| 3 | tanker / construction_vehicle |
|
||||
| 4 | unknown |
|
||||
| 5 | pedestrian |
|
||||
| 6 | bicycle / motorcycle |
|
||||
| 7 | motorcyclist / bicyclist |
|
||||
| 8 | tricycle / tricyclist |
|
||||
| 9 | traffic_sign |
|
||||
| 10 | wheel |
|
||||
| 11 | plate |
|
||||
| 12 | face |
|
||||
|
||||
类别 0–4 属于 `face_3d_classes`(面型3D重建),类别 5–8 属于 `complete_3d_classes`(整体3D重建)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 鱼眼镜头支持
|
||||
|
||||
若相机标定文件中包含有效的 `distort_coeffs`(4个系数 [k1, k2, k3, k4]),则:
|
||||
- 2D 坐标系下 3D 点投影时使用鱼眼畸变正向模型(`apply_fisheye_distortion`);
|
||||
- 2D → 3D 反投影时使用 Newton 迭代法求解畸变逆映射(`remove_fisheye_distortion`);
|
||||
- 3D 包围盒可视化时对各条棱进行密集采样后逐点投影,确保曲线正确。
|
||||
690
tools/model_inference/docs/two_roi_model_design_with_diff_and_fake3d.md
Executable file
690
tools/model_inference/docs/two_roi_model_design_with_diff_and_fake3d.md
Executable file
@@ -0,0 +1,690 @@
|
||||
# 双ROI导出模型设计说明(Diff 与 Fake3D 版本)
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
本文档说明当前新版双 ROI 合并导出模型在部署侧的输入约定、输出张量、后处理流程,以及 `difficulty` 分支与 `fake 3D` 分支的运行时语义。
|
||||
|
||||
本文档覆盖的对象是:
|
||||
|
||||
- `tools/model_merging/merge_models_of_2roi_yolo26.py`
|
||||
- `tools/model_inference/core/run_two_roi_exported_onnx_infer.py`
|
||||
- `tools/model_inference/core/two_roi_infer_utils.py`
|
||||
- `ultralytics/nn/modules/head.py`
|
||||
|
||||
凡与旧文档冲突之处,以当前代码实现为准。
|
||||
|
||||
如只查看旧版双 ROI 合并模型,请参考:
|
||||
|
||||
- `tools/model_inference/docs/two_roi_model_design.md`
|
||||
|
||||
---
|
||||
|
||||
## 2. 版本背景
|
||||
|
||||
当前仓库中的双 ROI 合并导出模型已经从旧版的:
|
||||
|
||||
- `2D boxes + class scores + 3D branch (+ 可选 edge branch)`
|
||||
|
||||
演进为新版可选扩展的形态:
|
||||
|
||||
- `2D boxes + class scores + 3D branch + difficulty branch`
|
||||
- `2D boxes + class scores + 3D branch + difficulty branch + fake 3D branch`
|
||||
- `(+ 可选 edge branch)`
|
||||
|
||||
其中:
|
||||
|
||||
- `difficulty branch` 用于输出目标 difficulty 二分类 logit
|
||||
- `fake 3D branch` 用于输出 fake class 专用的 3D 预测
|
||||
|
||||
当前合并导出脚本支持以下控制项:
|
||||
|
||||
- `--edge-head-mode keep|drop`
|
||||
- `--fake-3d-branch-mode keep|drop`
|
||||
|
||||
因此新版模型存在两条主要导出变体:
|
||||
|
||||
1. `drop fake_3d`:导出 `3D + diff`
|
||||
2. `keep fake_3d`:导出 `3D + diff + fake_3d`
|
||||
|
||||
---
|
||||
|
||||
## 3. 方案概览
|
||||
|
||||
当前运行时仍然沿用双 ROI 单目 3D 检测的总体思路,但部署形态已经收敛为:
|
||||
|
||||
1. 原图按 `ROI0` 和 `ROI1` 两套规则分别裁剪
|
||||
2. 两个 ROI 图像送入同一个合并导出模型
|
||||
3. 模型输出每个 ROI 的原始检测头张量
|
||||
4. 2D 框解码、Top-K、3D 反归一化、深度恢复、3D 框重建、fake-class 路由、可视化与 JSON 序列化全部在 Python 侧完成
|
||||
|
||||
当前支持:
|
||||
|
||||
- ONNX:`onnxruntime`
|
||||
- TorchScript:`torch.jit.load`
|
||||
- 单 case 推理
|
||||
- mined / eval 数据集批量推理
|
||||
|
||||
当前部署脚本不依赖训练时的 `ultralytics` dataloader,只依赖导出张量契约和运行时元数据。
|
||||
|
||||
---
|
||||
|
||||
## 4. 模型与运行时契约
|
||||
|
||||
### 4.1 导出模型形态
|
||||
|
||||
当前推理脚本只接受双 ROI 合并后的导出模型:
|
||||
|
||||
- `.onnx`
|
||||
- `.torchscript` / `.ts` / `.jit`
|
||||
|
||||
脚本会读取导出清单:
|
||||
|
||||
- ONNX 优先读取 sidecar:`merged_model.export.json`
|
||||
- TorchScript 优先读 sidecar,若 sidecar 缺失再读取模型内嵌 `config.txt`
|
||||
|
||||
当前部署脚本会显式要求:
|
||||
|
||||
```text
|
||||
export_mode == "raw_head_outputs"
|
||||
```
|
||||
|
||||
也就是说,当前文档只讨论原始检测头输出模式,不讨论 `hybrid_outputs`、`postprocessed_outputs` 等其他导出形态。
|
||||
|
||||
### 4.2 输入名称
|
||||
|
||||
默认输入名为:
|
||||
|
||||
- `roi0_input`
|
||||
- `roi1_input`
|
||||
|
||||
默认输入尺寸为:
|
||||
|
||||
- `768 x 352`
|
||||
|
||||
若导出清单里提供了 `input_names` 和 `input_sizes_wh`,运行时以清单为准。
|
||||
|
||||
### 4.3 导出控制开关
|
||||
|
||||
当前导出脚本暴露两个关键开关:
|
||||
|
||||
```bash
|
||||
--edge-head-mode keep|drop
|
||||
--fake-3d-branch-mode keep|drop
|
||||
```
|
||||
|
||||
它们会直接影响导出后的输出张量数量与名称。
|
||||
|
||||
### 4.4 运行时元数据
|
||||
|
||||
运行时会从数据配置 YAML 中读取以下元数据:
|
||||
|
||||
- `class_map`
|
||||
- `face_3d_classes`
|
||||
- `complete_3d_classes`
|
||||
- `fake_3d_classes`
|
||||
- `norm_scales_3d`
|
||||
|
||||
若未提供额外 YAML,则使用 `tools/model_inference/core/two_roi_infer_utils.py` 中的 `DEFAULT_DATASET_CONFIG`。
|
||||
|
||||
当前内置默认值包括:
|
||||
|
||||
- `face_3d_classes = [0,1,2,3,4,5,6,7,8,17]`
|
||||
- `complete_3d_classes = [9,10,11,12,18,19]`
|
||||
- `fake_3d_classes = [17,18,19]`
|
||||
|
||||
对应 fake 类别名称:
|
||||
|
||||
- `car_fake`
|
||||
- `bicyclist_fake`
|
||||
- `pedestrian_fake`
|
||||
|
||||
### 4.5 新旧版本输出类别对比
|
||||
|
||||
当前部署侧的 `cls_id` / `cls_name` 由 `class_map` 决定。
|
||||
旧版与新版的主要差异不只是输出分支数量,还包括默认类别集合本身已经扩展。
|
||||
|
||||
旧版默认类别定义为:
|
||||
|
||||
| `cls_id` | 旧版默认类别 |
|
||||
|---|---|
|
||||
| `0` | `car` |
|
||||
| `1` | `suv` |
|
||||
| `2` | `pickup` |
|
||||
| `3` | `medium_car` |
|
||||
| `4` | `van` |
|
||||
| `5` | `bus` |
|
||||
| `6` | `truck` / `tanker` / `large_truck` / `construction_vehicle` |
|
||||
| `7` | `special_vehicle` |
|
||||
| `8` | `unknown` |
|
||||
| `9` | `pedestrian` |
|
||||
| `10` | `bicyclist` / `motorcyclist` |
|
||||
| `11` | `bicycle` / `motorcycle` |
|
||||
| `12` | `tricycle` / `tricyclist` |
|
||||
| `13` | `traffic_sign` |
|
||||
| `14` | `wheel` |
|
||||
| `15` | `plate` |
|
||||
| `16` | `face` |
|
||||
|
||||
新版默认类别定义为:
|
||||
|
||||
| `cls_id` | 新版默认类别 |
|
||||
|---|---|
|
||||
| `0-16` | 与旧版相同 |
|
||||
| `17` | `car_fake` |
|
||||
| `18` | `bicyclist_fake` |
|
||||
| `19` | `pedestrian_fake` |
|
||||
|
||||
因此默认 `scores_head_raw` 的类别数从旧版的:
|
||||
|
||||
- `nc = 17`
|
||||
|
||||
扩展为新版的:
|
||||
|
||||
- `nc = 20`
|
||||
|
||||
对应的 3D 类别分组也从旧版:
|
||||
|
||||
- `face_3d_classes = [0,1,2,3,4,5,6,7,8]`
|
||||
- `complete_3d_classes = [9,10,11,12]`
|
||||
|
||||
扩展为新版:
|
||||
|
||||
- `face_3d_classes = [0,1,2,3,4,5,6,7,8,17]`
|
||||
- `complete_3d_classes = [9,10,11,12,18,19]`
|
||||
- `fake_3d_classes = [17,18,19]`
|
||||
|
||||
语义上可以理解为:
|
||||
|
||||
- `0-16`:延续旧版类别语义
|
||||
- `17-19`:新增 fake 类别,2D 分类仍走统一 `scores_head_raw`
|
||||
- `17` 会按 face 类路径参与 3D 解码,但在保留 fake 分支时优先读取 `preds_3d_fake_head_raw`
|
||||
- `18-19` 会按 complete 类路径参与 3D 解码,但在保留 fake 分支时同样优先读取 `preds_3d_fake_head_raw`
|
||||
|
||||
---
|
||||
|
||||
## 5. ROI 与前处理设计
|
||||
|
||||
### 5.1 默认 ROI 配置
|
||||
|
||||
默认 ROI 配置来自 `DEFAULT_DATASET_CONFIG["roi_configs"]`:
|
||||
|
||||
| ROI | 裁剪尺寸 `(w, h)` | 裁剪中心模式 | `virtual_fx` | 默认输入尺寸 `(w, h)` |
|
||||
|---|---:|---|---:|---:|
|
||||
| `ROI0` | `1920 x 880` | `cxvy` | `537.0` | `768 x 352` |
|
||||
| `ROI1` | `768 x 352` | `vxvy` | `537.0` | `768 x 352` |
|
||||
|
||||
其中:
|
||||
|
||||
- `cxvy`:裁剪中心 `x` 取图像水平中心,`y` 取灭点 `vp_y`
|
||||
- `vxvy`:裁剪中心 `x` 取灭点 `vp_x`,`y` 取灭点 `vp_y`
|
||||
|
||||
### 5.2 标定与灭点
|
||||
|
||||
当前运行时兼容扁平 `camera4.json` 与合并格式 `camera4.json`。
|
||||
|
||||
灭点计算逻辑:
|
||||
|
||||
```text
|
||||
vp_x = cu + focal_u * tan(yaw)
|
||||
vp_y = cv - focal_v * tan(pitch)
|
||||
```
|
||||
|
||||
### 5.3 ROI 裁剪
|
||||
|
||||
裁剪边界通过 `compute_centered_roi_bounds()` 计算:
|
||||
|
||||
```text
|
||||
crop_x1 = clamp(center_x - roi_w / 2, 0, ori_w - roi_w)
|
||||
crop_y1 = clamp(center_y - roi_h / 2, 0, ori_h - roi_h)
|
||||
crop_x2 = crop_x1 + roi_w
|
||||
crop_y2 = crop_y1 + roi_h
|
||||
```
|
||||
|
||||
### 5.4 resize 与标定更新
|
||||
|
||||
当前脚本会把 ROI 图像 resize 到模型输入尺寸,并同步更新 resized ROI 坐标系下的标定:
|
||||
|
||||
```text
|
||||
fx = focal_u * scale_x
|
||||
fy = focal_v * scale_y
|
||||
cx = (cu - crop_x1) * scale_x
|
||||
cy = (cv - crop_y1) * scale_y
|
||||
depth_scale = fx / virtual_fx
|
||||
```
|
||||
|
||||
其中 `depth_scale` 会在 3D 后处理阶段恢复真实焦距下的深度尺度。
|
||||
|
||||
### 5.5 输入张量化
|
||||
|
||||
输入张量构造方式:
|
||||
|
||||
```python
|
||||
image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
|
||||
array = image_rgb.transpose(2, 0, 1).astype(np.float32) / 256.0
|
||||
input = array[None, ...]
|
||||
```
|
||||
|
||||
注意是:
|
||||
|
||||
```text
|
||||
/ 256.0
|
||||
```
|
||||
|
||||
不是 `/ 255.0`。
|
||||
|
||||
---
|
||||
|
||||
## 6. 输出张量设计
|
||||
|
||||
### 6.1 总体原则
|
||||
|
||||
当前部署脚本只支持每个 ROI 输出原始检测头张量。
|
||||
输出张量是否包含 `preds_edge`、`preds_3d_fake` 由导出开关控制。
|
||||
|
||||
每个 ROI 至少包含:
|
||||
|
||||
1. `boxes_head_raw`
|
||||
2. `scores_head_raw`
|
||||
3. `preds_3d_head_raw`
|
||||
4. `preds_diff_head_raw`
|
||||
|
||||
### 6.2 `drop fake_3d` 且 `drop edge`
|
||||
|
||||
当:
|
||||
|
||||
```bash
|
||||
--fake-3d-branch-mode drop
|
||||
--edge-head-mode drop
|
||||
```
|
||||
|
||||
合并模型总输出为 8 个:
|
||||
|
||||
1. `roi0_boxes_head_raw`
|
||||
2. `roi0_scores_head_raw`
|
||||
3. `roi0_preds_3d_head_raw`
|
||||
4. `roi0_preds_diff_head_raw`
|
||||
5. `roi1_boxes_head_raw`
|
||||
6. `roi1_scores_head_raw`
|
||||
7. `roi1_preds_3d_head_raw`
|
||||
8. `roi1_preds_diff_head_raw`
|
||||
|
||||
### 6.3 `keep fake_3d` 且 `drop edge`
|
||||
|
||||
当:
|
||||
|
||||
```bash
|
||||
--fake-3d-branch-mode keep
|
||||
--edge-head-mode drop
|
||||
```
|
||||
|
||||
合并模型总输出为 10 个:
|
||||
|
||||
1. `roi0_boxes_head_raw`
|
||||
2. `roi0_scores_head_raw`
|
||||
3. `roi0_preds_3d_head_raw`
|
||||
4. `roi0_preds_diff_head_raw`
|
||||
5. `roi0_preds_3d_fake_head_raw`
|
||||
6. `roi1_boxes_head_raw`
|
||||
7. `roi1_scores_head_raw`
|
||||
8. `roi1_preds_3d_head_raw`
|
||||
9. `roi1_preds_diff_head_raw`
|
||||
10. `roi1_preds_3d_fake_head_raw`
|
||||
|
||||
### 6.4 `keep edge` 的扩展
|
||||
|
||||
若再加:
|
||||
|
||||
```bash
|
||||
--edge-head-mode keep
|
||||
```
|
||||
|
||||
则每个 ROI 还会额外多一个:
|
||||
|
||||
- `roi*_preds_edge_head_raw`
|
||||
|
||||
### 6.5 典型 shape
|
||||
|
||||
对默认输入尺寸 `768 x 352`:
|
||||
|
||||
```text
|
||||
A = (352/8 * 768/8) + (352/16 * 768/16) + (352/32 * 768/32)
|
||||
= 4224 + 1056 + 264
|
||||
= 5544
|
||||
```
|
||||
|
||||
所以典型输出 shape 为:
|
||||
|
||||
| 输出名 | 形状 | 含义 |
|
||||
|---|---|---|
|
||||
| `roi*_boxes_head_raw` | `[1, 4, 5544]` 或 `[1, 4*reg_max, 5544]` | 2D 框回归头原始输出 |
|
||||
| `roi*_scores_head_raw` | `[1, nc, 5544]` | 分类原始 logits,旧版默认 `nc=17`,新版默认 `nc=20` |
|
||||
| `roi*_preds_3d_head_raw` | `[1, 41, 5544]` | 常规 3D 分支 |
|
||||
| `roi*_preds_diff_head_raw` | `[1, 1, 5544]` | difficulty 二分类分支 |
|
||||
| `roi*_preds_3d_fake_head_raw` | `[1, 41, 5544]` | fake class 专用 3D 分支 |
|
||||
| `roi*_preds_edge_head_raw` | `[1, 60, 5544]` | edge 分支 |
|
||||
|
||||
注意:
|
||||
|
||||
- 实际 `boxes_head_raw` 通道数取决于 `reg_max`
|
||||
- 旧版本文档里常见的 `[1, 4*reg_max, 5544]` 写法仍然成立
|
||||
- 你当前部分导出模型如果已在导出前把 `reg_max` 相关结构折叠,也可能看到 `[1, 4, 5544]`
|
||||
|
||||
### 6.6 各分支语义
|
||||
|
||||
#### 6.6.1 `boxes_head_raw`
|
||||
|
||||
2D 框回归头的原始输出,逐 anchor 表示边界框回归量。
|
||||
运行时需要结合 anchor 网格与 stride 做 decode,不能直接作为最终 bbox。
|
||||
|
||||
#### 6.6.2 `scores_head_raw`
|
||||
|
||||
分类头的原始 logits,逐 anchor、逐类输出。
|
||||
运行时会先做 sigmoid,再走 one-to-one Top-K 选择。
|
||||
|
||||
#### 6.6.3 `preds_3d_head_raw`
|
||||
|
||||
常规 3D 分支,适用于常规 3D 类别。
|
||||
该分支输出 41 维 3D 表达,后续会按 `norm_scales_3d` 做反归一化。
|
||||
|
||||
#### 6.6.4 `preds_diff_head_raw`
|
||||
|
||||
difficulty 分支输出,每个 anchor 只有 1 维 logit。
|
||||
当前训练逻辑中,difficulty 被视为二分类:
|
||||
|
||||
- `0/1 -> easy-like`
|
||||
- `2/3 -> hard-like`
|
||||
|
||||
部署侧目前保留该分支输出,但默认主推理可视化链路还不会直接把它画到结果图上。
|
||||
|
||||
#### 6.6.5 `preds_3d_fake_head_raw`
|
||||
|
||||
fake class 专用 3D 分支。
|
||||
该分支与常规 3D 分支一样,也是 41 维,但只用于 fake 类别:
|
||||
|
||||
- `car_fake`
|
||||
- `bicyclist_fake`
|
||||
- `pedestrian_fake`
|
||||
|
||||
当导出模型包含这个张量且当前 detection 的 `cls_id` 落在 `fake_3d_classes` 中时,运行时会优先用它,而不是 `preds_3d_head_raw`。
|
||||
|
||||
#### 6.6.6 `preds_edge_head_raw`
|
||||
|
||||
edge 分支输出 60 维。
|
||||
每个面的底边缘由 5 个采样点组成,每个采样点 3 维:
|
||||
|
||||
```text
|
||||
[du, dv, z]
|
||||
```
|
||||
|
||||
4 个面一共 60 维。
|
||||
|
||||
---
|
||||
|
||||
## 7. 3D 分支定义
|
||||
|
||||
### 7.1 常规 3D 分支(41 维)
|
||||
|
||||
通道布局:
|
||||
|
||||
| 通道范围 | 含义 |
|
||||
|---|---|
|
||||
| `0-5` | front face:`z3d, u_offset, v_offset, h, w, visible_score` |
|
||||
| `6-11` | rear face:`z3d, u_offset, v_offset, h, w, visible_score` |
|
||||
| `12-17` | left face:`z3d, u_offset, v_offset, l, h, visible_score` |
|
||||
| `18-23` | right face:`z3d, u_offset, v_offset, l, h, visible_score` |
|
||||
| `24` | whole-box `z3d` |
|
||||
| `25-26` | whole-box `u_offset, v_offset` |
|
||||
| `27-29` | whole-box `l, h, w` |
|
||||
| `30-33` | yaw 4-bin 分类 logits |
|
||||
| `34-37` | yaw 残差 `sin(delta)` |
|
||||
| `38-40` | cut state logits |
|
||||
|
||||
### 7.2 fake 3D 分支(41 维)
|
||||
|
||||
`preds_3d_fake_head_raw` 与 `preds_3d_head_raw` 在张量结构上完全一致,也是 41 维。
|
||||
区别只在于用途:
|
||||
|
||||
- `preds_3d_head_raw`:常规类别使用
|
||||
- `preds_3d_fake_head_raw`:fake class 使用
|
||||
|
||||
### 7.3 edge 分支(60 维)
|
||||
|
||||
| 面 | 通道范围 |
|
||||
|---|---|
|
||||
| front | `0-14` |
|
||||
| rear | `15-29` |
|
||||
| left | `30-44` |
|
||||
| right | `45-59` |
|
||||
|
||||
---
|
||||
|
||||
## 8. 后处理设计
|
||||
|
||||
### 8.1 2D 框解码
|
||||
|
||||
运行时用 `decode_boxes_xyxy()` 对 `boxes_head_raw` 解码:
|
||||
|
||||
1. 还原 `l, t, r, b`
|
||||
2. 与 anchor 网格中心组合
|
||||
3. 乘以 stride 还原到 ROI-resized 图像坐标
|
||||
|
||||
anchor 由 `build_anchor_cache()` 预生成,默认 stride:
|
||||
|
||||
```text
|
||||
(8, 16, 32)
|
||||
```
|
||||
|
||||
### 8.2 Top-K 选择
|
||||
|
||||
当前推理脚本沿用 one-to-one Top-K 路径:
|
||||
|
||||
1. 对分类 logits 做 sigmoid
|
||||
2. 每个 anchor 取最大类别分数
|
||||
3. 做 Top-K
|
||||
4. 生成 `detections = [x1, y1, x2, y2, conf, cls_id]`
|
||||
|
||||
### 8.3 3D / Edge 反归一化
|
||||
|
||||
`preds_3d_head_raw`、`preds_3d_fake_head_raw` 与 `preds_edge_head_raw` 会用 `norm_scales_3d` 做反归一化:
|
||||
|
||||
| 项目 | 反归一化方式 |
|
||||
|---|---|
|
||||
| `z3d` | `raw * z3d_scale + z3d_offset` |
|
||||
| `u/v offset` | `sigmoid(raw) * 16 - 8` |
|
||||
| `size` | `raw * size_scale + size_offset` |
|
||||
| yaw residual | `tanh(raw)` |
|
||||
| edge `z` | `raw * z3d_scale + z3d_offset` |
|
||||
| edge `du/dv` | `sigmoid(raw) * 16 - 8` |
|
||||
|
||||
默认值:
|
||||
|
||||
```text
|
||||
z3d_scale = 24.415
|
||||
z3d_offset = 39.937
|
||||
size_scale = 1.945
|
||||
size_offset = 3.780
|
||||
yaw_scale = 1.5707963
|
||||
```
|
||||
|
||||
### 8.4 深度恢复
|
||||
|
||||
部署时需要按真实焦距恢复深度尺度:
|
||||
|
||||
```text
|
||||
preds_3d[:, (0, 6, 12, 18, 24)] *= depth_scale
|
||||
preds_3d_fake[:, (0, 6, 12, 18, 24)] *= depth_scale
|
||||
preds_edge[:, 2::3] *= depth_scale
|
||||
```
|
||||
|
||||
其中:
|
||||
|
||||
```text
|
||||
depth_scale = fx / virtual_fx
|
||||
```
|
||||
|
||||
### 8.5 fake class 的 3D 路由
|
||||
|
||||
这是新版运行时与旧版的关键差异之一。
|
||||
|
||||
当前 exported inference 在逐检测框解码时,会做如下判断:
|
||||
|
||||
1. 读取 detection 的 `cls_id`
|
||||
2. 若 `cls_id in fake_3d_classes`
|
||||
3. 且当前导出模型包含 `preds_3d_fake`
|
||||
4. 则用 `preds_3d_fake` 作为该框的 3D 解码输入
|
||||
5. 否则继续使用 `preds_3d`
|
||||
|
||||
也就是说:
|
||||
|
||||
- `keep fake_3d` 时:fake 类别走专用 3D 分支
|
||||
- `drop fake_3d` 时:所有类别都走主 `preds_3d`
|
||||
|
||||
### 8.6 difficulty 分支的使用
|
||||
|
||||
当前部署侧已经能把 `preds_diff` 读入运行时结果结构:
|
||||
|
||||
- `ROISelectedPredictions.preds_diff`
|
||||
|
||||
但默认主推理链路暂未把它用于:
|
||||
|
||||
- 过滤
|
||||
- 合并
|
||||
- 可视化绘制
|
||||
- JSON 输出主字段
|
||||
|
||||
因此当前它的状态是:
|
||||
|
||||
- 已导出
|
||||
- 已可解析
|
||||
- 已可供后续扩展
|
||||
- 默认主链路尚未深度消费
|
||||
|
||||
### 8.7 edge yaw 与 edge box
|
||||
|
||||
若导出模型包含 edge 分支,则运行时可继续执行 edge-based 诊断链:
|
||||
|
||||
1. 反投影可见底边缘点
|
||||
2. 选择单面或双面组合
|
||||
3. 计算 `edge_yaw`
|
||||
4. 约束横向距离阈值
|
||||
5. 重建 `edge_box`
|
||||
6. 生成额外诊断信息
|
||||
|
||||
若使用:
|
||||
|
||||
```bash
|
||||
--edge-head-mode drop
|
||||
```
|
||||
|
||||
则该能力在部署侧被关闭。
|
||||
|
||||
---
|
||||
|
||||
## 9. 下游解析逻辑
|
||||
|
||||
### 9.1 新旧输出协议兼容
|
||||
|
||||
当前 `run_two_roi_exported_onnx_infer.py` 对以下情况都兼容:
|
||||
|
||||
1. 旧版仅 `boxes + scores + preds_3d`
|
||||
2. 新版 `boxes + scores + preds_3d + preds_diff`
|
||||
3. 新版 `boxes + scores + preds_3d + preds_diff + preds_3d_fake`
|
||||
4. 上述版本再叠加 `preds_edge`
|
||||
|
||||
兼容方式为:
|
||||
|
||||
- 必需输出只要求旧版最小集合
|
||||
- 若发现 `preds_3d_fake` / `preds_diff` / `preds_edge` 则自动启用
|
||||
- 若缺失则退化为旧行为
|
||||
|
||||
### 9.2 建议的外部调用方式
|
||||
|
||||
外部调用方不要按固定 output index 写死解析逻辑。
|
||||
推荐使用:
|
||||
|
||||
1. 先读取 `merged_model.export.json`
|
||||
2. 按 `output_names` 解析输出
|
||||
|
||||
原因是:
|
||||
|
||||
- `keep/drop fake_3d_branch` 会改变输出总数
|
||||
- `keep/drop edge_head` 也会改变输出总数
|
||||
|
||||
---
|
||||
|
||||
## 10. 与旧版文档的主要差异
|
||||
|
||||
相对旧版双 ROI 文档,当前新版多出以下变化:
|
||||
|
||||
1. 新增 `preds_diff_head_raw`
|
||||
2. 新增可选 `preds_3d_fake_head_raw`
|
||||
3. 默认输出类别从 `17` 类扩展到 `20` 类,新增:
|
||||
- `car_fake`
|
||||
- `bicyclist_fake`
|
||||
- `pedestrian_fake`
|
||||
4. 运行时引入 `fake_3d_classes`
|
||||
5. fake 类别不再强制走主 `preds_3d`
|
||||
6. 导出 manifest 中新增:
|
||||
- `fake_3d_branch_mode`
|
||||
- `keep_fake_3d_branch`
|
||||
7. 输出总数不再固定为 8
|
||||
|
||||
典型情况:
|
||||
|
||||
- 旧版 no-edge:8 输出
|
||||
- 新版 drop fake + drop edge:8 输出
|
||||
- 新版 keep fake + drop edge:10 输出
|
||||
- 新版 keep fake + keep edge:12 输出
|
||||
|
||||
---
|
||||
|
||||
## 11. 导出示例
|
||||
|
||||
### 11.1 保留 fake 3D 分支
|
||||
|
||||
```bash
|
||||
python tools/model_merging/merge_models_of_2roi_yolo26.py \
|
||||
--roi0-model-path runs/detect/mono3d_roi0_20260506_epoch99.pt \
|
||||
--roi1-model-path runs/detect/mono3d_roi1_20260506_epoch99.pt \
|
||||
--save-dir runs/export/train_mono3d_two_roi_20260506-keep_fake_3d_branch \
|
||||
--imgsz 768 352 \
|
||||
--edge-head-mode drop \
|
||||
--fake-3d-branch-mode keep
|
||||
```
|
||||
|
||||
输出:
|
||||
|
||||
- `boxes`
|
||||
- `scores`
|
||||
- `preds_3d`
|
||||
- `preds_diff`
|
||||
- `preds_3d_fake`
|
||||
|
||||
### 11.2 去掉 fake 3D 分支
|
||||
|
||||
```bash
|
||||
python tools/model_merging/merge_models_of_2roi_yolo26.py \
|
||||
--roi0-model-path runs/detect/mono3d_roi0_20260506_epoch99.pt \
|
||||
--roi1-model-path runs/detect/mono3d_roi1_20260506_epoch99.pt \
|
||||
--save-dir runs/export/train_mono3d_two_roi_20260506-drop_fake_3d_branch \
|
||||
--imgsz 768 352 \
|
||||
--edge-head-mode drop \
|
||||
--fake-3d-branch-mode drop
|
||||
```
|
||||
|
||||
输出:
|
||||
|
||||
- `boxes`
|
||||
- `scores`
|
||||
- `preds_3d`
|
||||
- `preds_diff`
|
||||
|
||||
---
|
||||
|
||||
## 12. 建议与注意事项
|
||||
|
||||
1. 如果下游要使用 fake class 的专用 3D 几何,请使用 `--fake-3d-branch-mode keep`。
|
||||
2. 如果只想保持旧版解析复杂度,可使用 `--fake-3d-branch-mode drop`。
|
||||
3. 如果外部部署程序不是本仓库当前的 `run_two_roi_exported_onnx_infer.py`,请务必按 manifest 的 `output_names` 解析。
|
||||
4. `preds_diff` 当前默认未用于主要可视化与合并策略,但导出时建议保留,便于后续直接扩展。
|
||||
5. `keep fake_3d` 与 `drop fake_3d` 的输出总数不同,不能混用固定 index 的后处理脚本。
|
||||
272
tools/model_inference/docs/two_roi_range_oscillation_solution.md
Executable file
272
tools/model_inference/docs/two_roi_range_oscillation_solution.md
Executable file
@@ -0,0 +1,272 @@
|
||||
# 单目3D测距周期性波动问题分析与解决方案
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
本文档聚焦当前双 ROI 单目 3D 检测部署方案中出现的“目标测距周期性波动”问题,结合现有实现分析可能成因,并给出可落地的改进方案与推荐推进顺序。
|
||||
|
||||
本文档不替代 `tools/model_inference/docs/two_roi_model_design.md`。凡涉及当前部署链路的事实性描述,仍以现有推理脚本及其依赖模块为准。
|
||||
|
||||
---
|
||||
|
||||
## 2. 问题描述
|
||||
|
||||
当前模型在部分场景下会出现如下现象:
|
||||
|
||||
- 同一目标在连续帧中的测距结果呈现周期性起伏
|
||||
- 目标实际距离变化不大,但 `z3d` 或 `center_3d.z` 存在明显波动
|
||||
- 波动往往与车辆颠簸、路面不平、减速带、桥头跳车等场景同步出现
|
||||
|
||||
初步分析表明,该问题很可能与自车俯仰姿态变化有关。当自车颠簸时,目标在图像中的垂直位置会发生上下起伏,而当前单帧单目 3D 模型会将这类图像变化部分解释为距离变化。
|
||||
|
||||
---
|
||||
|
||||
## 3. 与当前实现的关系
|
||||
|
||||
### 3.1 当前实现中的关键链路
|
||||
|
||||
结合现有设计与代码实现,测距波动问题存在以下可能传播链路:
|
||||
|
||||
1. 当前灭点计算依赖静态标定中的 `pitch`
|
||||
|
||||
```text
|
||||
vp_y = cv - focal_v * tan(pitch)
|
||||
```
|
||||
|
||||
2. 当前 `camera4.json` 在 case 开始时加载一次,后续帧默认复用同一份标定,不会按帧更新 `pitch / roll`。
|
||||
|
||||
3. 当前 ROI 裁剪在 `y` 方向上直接依赖 `vp_y`,也就是依赖静态俯仰角计算出的灭点位置。
|
||||
|
||||
4. ROI 裁剪和 resize 后,会重新计算 `fx / fy / cx / cy`,后续 3D 回投和深度恢复仍继续使用这套 ROI 坐标系下的标定。
|
||||
|
||||
5. 当真实车身姿态随时间变化、但运行时仍使用静态姿态来解释图像时,目标的上下漂移会被系统误解释为深度变化,最终表现为 `z3d` 周期性波动。
|
||||
|
||||
### 3.2 问题本质
|
||||
|
||||
问题本质可以概括为:
|
||||
|
||||
- 当前实现默认相机姿态在时序上稳定
|
||||
- 实际车辆在颠簸场景下存在动态 `pitch / roll`
|
||||
- 单帧视觉观测中,目标垂直位移与真实深度变化发生耦合
|
||||
|
||||
因此,该问题既是“相机姿态建模不足”的问题,也是“单帧深度估计对图像上下扰动敏感”的问题。
|
||||
|
||||
---
|
||||
|
||||
## 4. 解决方案分层
|
||||
|
||||
从工程与算法角度,可以将方案分为五层:逐帧姿态补偿、时序平滑、ROI 抗扰动、训练增强、几何一致性融合。
|
||||
|
||||
### 4.1 逐帧姿态补偿
|
||||
|
||||
这是最接近根因修复的方案。
|
||||
|
||||
核心思路:
|
||||
|
||||
- 获取逐帧 `pitch / roll`
|
||||
- 按帧更新灭点位置、ROI 裁剪中心和相机姿态
|
||||
- 必要时在进入模型前先完成图像稳定,再执行 ROI 裁剪与推理
|
||||
|
||||
可选实现路径:
|
||||
|
||||
- 接入 `IMU / CAN` 的实时姿态信号
|
||||
- 无额外传感器时,基于地平线、车道线、路面结构、稳定背景点估计视觉姿态
|
||||
|
||||
建议改造点:
|
||||
|
||||
- 将静态 `camera4.json` 扩展为逐帧动态姿态输入
|
||||
- 按帧更新 `vp_y`
|
||||
- 按帧更新 ROI 的 `crop_center_y`
|
||||
- 若结果需要映射到 ego 坐标系,则同步更新相机到 ego 的姿态变换
|
||||
|
||||
优点:
|
||||
|
||||
- 直接处理误差源头
|
||||
- 同时改善 ROI 对位与 3D 回投
|
||||
|
||||
缺点:
|
||||
|
||||
- 依赖新增姿态源或新增视觉姿态估计模块
|
||||
- 集成复杂度最高
|
||||
|
||||
### 4.2 Track 级时序平滑
|
||||
|
||||
这是最适合快速落地的工程方案。
|
||||
|
||||
核心思路:
|
||||
|
||||
- 不直接使用单帧 `z3d` 作为最终结果
|
||||
- 先对目标建立 track
|
||||
- 再对同一目标的连续帧 3D 结果做时序滤波
|
||||
|
||||
推荐方式:
|
||||
|
||||
- 基于 2D/3D 关联建立 tracking
|
||||
- 对 `center_3d.z`、`center_3d.x`、`yaw` 等量做 `EMA`、`Kalman Filter` 或固定窗平滑
|
||||
- 根据类别、置信度、框尺寸、edge 几何一致性等动态调整测量噪声
|
||||
|
||||
优点:
|
||||
|
||||
- 不需要修改训练
|
||||
- 工程改造范围小
|
||||
- 对周期性抖动通常能快速见效
|
||||
|
||||
缺点:
|
||||
|
||||
- 只能抑制表现,不能消除根因
|
||||
- 会引入一定延迟
|
||||
|
||||
### 4.3 降低 ROI 对上下抖动的敏感性
|
||||
|
||||
当前 ROI 在 `y` 方向上缺少独立的稳定策略,因此可以先做轻量改造。
|
||||
|
||||
可选方案:
|
||||
|
||||
- 引入独立的 `crop_center_y_mode`
|
||||
- 支持 `fixed_cy`
|
||||
- 支持 `smoothed_vpy`
|
||||
- 支持 `blend(cv, vp_y)` 形式的加权中心
|
||||
- 对 `crop_center_y` 做低通滤波或逐帧最大步长限制
|
||||
- 适当增大 ROI 高度,降低轻微颠簸引入的相对位置扰动
|
||||
|
||||
优点:
|
||||
|
||||
- 代码改动较小
|
||||
- 便于快速验证问题是否主要由 ROI 跟随静态灭点触发
|
||||
|
||||
缺点:
|
||||
|
||||
- 只能缓解,不能完整表达真实动态姿态
|
||||
|
||||
### 4.4 训练侧抗颠簸增强
|
||||
|
||||
若该问题在量产场景中普遍存在,建议在训练侧显式纳入姿态扰动域。
|
||||
|
||||
建议方向:
|
||||
|
||||
- 增加 `pitch / roll` 扰动增强
|
||||
- 增加垂直平移、轻微透视变化、动态模糊等增强
|
||||
- 对连续帧加入时序一致性约束
|
||||
- 条件允许时,将姿态信息、地平线位置或多帧特征作为额外输入
|
||||
|
||||
进一步方案:
|
||||
|
||||
- 训练轻量多帧模型
|
||||
- 引入显式姿态辅助分支
|
||||
- 增加地平线或路面几何中间量预测
|
||||
|
||||
优点:
|
||||
|
||||
- 可以从模型层面提升鲁棒性
|
||||
|
||||
缺点:
|
||||
|
||||
- 需要重新训练和完整回归验证
|
||||
|
||||
### 4.5 几何一致性融合
|
||||
|
||||
对于车辆类目标,可将网络输出与几何约束融合,以降低单一回归分支对上下抖动的敏感性。
|
||||
|
||||
可利用信息包括:
|
||||
|
||||
- ground plane 或接地点约束
|
||||
- 目标底边缘与 edge 分支几何
|
||||
- 类别尺寸先验
|
||||
- 连续帧运动学先验
|
||||
|
||||
可选策略:
|
||||
|
||||
- 当 `z3d` 与 edge / ground 几何明显冲突时,对该帧深度降权
|
||||
- 将网络回归深度与几何估计深度做加权融合
|
||||
- 将异常跳变作为诊断信号输出
|
||||
|
||||
优点:
|
||||
|
||||
- 提高异常帧下的可解释性
|
||||
- 有助于构建更稳健的诊断链路
|
||||
|
||||
缺点:
|
||||
|
||||
- 依赖额外几何假设,适用范围需要单独验证
|
||||
|
||||
---
|
||||
|
||||
## 5. 推荐推进顺序
|
||||
|
||||
若目标是“先快速抑制线上波动,再逐步修复根因”,建议按以下顺序推进:
|
||||
|
||||
1. 先增加 `track` 级时序平滑,快速压制测距抖动
|
||||
2. 同时引入 ROI `y` 向稳定策略,验证 ROI 机制的敏感性
|
||||
3. 若验证表明问题与车身俯仰强相关,再接入逐帧 `pitch / roll` 补偿
|
||||
4. 在训练侧加入抗颠簸增强,降低模型对垂直扰动的敏感性
|
||||
5. 视业务需要补充 edge / ground / 运动学等几何一致性融合
|
||||
|
||||
该顺序兼顾了:
|
||||
|
||||
- 短期可落地性
|
||||
- 中期定位能力
|
||||
- 长期根因修复效果
|
||||
|
||||
---
|
||||
|
||||
## 6. 推荐实施策略
|
||||
|
||||
从工程收益比看,建议优先采用“两阶段策略”。
|
||||
|
||||
### 6.1 第一阶段:快速止血
|
||||
|
||||
目标:
|
||||
|
||||
- 尽快降低线上测距周期波动
|
||||
- 快速判断问题是否主要与姿态扰动相关
|
||||
|
||||
建议动作:
|
||||
|
||||
- 增加 track 级滤波
|
||||
- 为 ROI 增加 `y` 向平滑或固定策略
|
||||
- 输出更多诊断字段,例如平滑前后深度、ROI 中心变化量、疑似姿态扰动标记
|
||||
|
||||
### 6.2 第二阶段:根因修复
|
||||
|
||||
目标:
|
||||
|
||||
- 将静态姿态假设升级为动态姿态建模
|
||||
|
||||
建议动作:
|
||||
|
||||
- 接入逐帧姿态输入
|
||||
- 按帧更新灭点、裁剪、回投与 ego 变换
|
||||
- 联合训练增强与几何融合进一步提升鲁棒性
|
||||
|
||||
---
|
||||
|
||||
## 7. 验证建议
|
||||
|
||||
为了判断方案是否有效,建议重点关注以下指标:
|
||||
|
||||
- 同一目标连续帧 `center_3d.z` 的标准差
|
||||
- 目标静稳跟车场景下的深度峰峰值
|
||||
- 颠簸场景中深度频谱的主频能量
|
||||
- 引入平滑后的延迟和响应速度
|
||||
- ego 坐标系下 `xyzlhwyaw` 的稳定性
|
||||
|
||||
建议单独构建以下评测子集:
|
||||
|
||||
- 路面平稳场景
|
||||
- 明显颠簸场景
|
||||
- 远距离车辆场景
|
||||
- 近距离跟车场景
|
||||
- 高速桥头或减速带场景
|
||||
|
||||
---
|
||||
|
||||
## 8. 结论
|
||||
|
||||
当前测距周期性波动问题,很大概率与“真实动态俯仰变化”和“运行时静态姿态假设”之间的不一致有关。
|
||||
|
||||
从方案优先级上看:
|
||||
|
||||
- 短期最有效的是 `track` 级时序平滑与 ROI `y` 向稳定
|
||||
- 中长期最关键的是逐帧姿态补偿
|
||||
- 若要进一步提升模型鲁棒性,应结合训练增强与几何一致性融合
|
||||
|
||||
其中,逐帧姿态补偿是最接近根因修复的方案;其余方案更适合作为快速缓解、问题定位和整体稳健性增强手段。
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user