单目3D初始代码
This commit is contained in:
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]
|
||||
Reference in New Issue
Block a user