单目3D初始代码
This commit is contained in:
301
eval_tools/core/eval.py
Executable file
301
eval_tools/core/eval.py
Executable file
@@ -0,0 +1,301 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
YOLOv5-3D Model Evaluation Script
|
||||
|
||||
This script evaluates 2D and 3D detection performance of YOLOv5-3D model.
|
||||
It computes metrics including Precision, Recall, AP, mAP for 2D detection,
|
||||
and lateral/longitudinal errors and heading errors for 3D detection.
|
||||
|
||||
Usage:
|
||||
# Basic usage with paths
|
||||
python eval_tools/eval.py --det-path /path/to/detections --gt-path /path/to/labels --output-dir results
|
||||
|
||||
# With configuration file
|
||||
python eval_tools/eval.py --config eval_tools/configs/eval_config.yaml
|
||||
|
||||
# Evaluate 2D only
|
||||
python eval_tools/eval.py --det-path /path/to/detections --gt-path /path/to/labels --eval-2d-only
|
||||
|
||||
# Evaluate 3D only
|
||||
python eval_tools/eval.py --det-path /path/to/detections --gt-path /path/to/labels --eval-3d-only
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from eval_tools.evaluator import Evaluator
|
||||
|
||||
|
||||
def load_config(config_path):
|
||||
"""Load configuration from YAML file."""
|
||||
with open(config_path, 'r') as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
# Replace timestamp placeholders in paths
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
if 'output' in config and 'save_path' in config['output']:
|
||||
config['output']['save_path'] = config['output']['save_path'].replace('{timestamp}', timestamp)
|
||||
if 'dataset' in config:
|
||||
if 'det_path' in config['dataset']:
|
||||
config['dataset']['det_path'] = config['dataset']['det_path'].replace('{timestamp}', timestamp)
|
||||
if 'gt_path' in config['dataset']:
|
||||
config['dataset']['gt_path'] = config['dataset']['gt_path'].replace('{timestamp}', timestamp)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def parse_arguments():
|
||||
"""Parse command line arguments."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Evaluate YOLOv5-3D model detection results',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Basic evaluation
|
||||
python eval_tools/eval.py --det-path /data/detections --gt-path /data/labels --output-dir results
|
||||
|
||||
# With custom image size
|
||||
python eval_tools/eval.py --det-path /data/detections --gt-path /data/labels --img-width 3840 --img-height 2160
|
||||
|
||||
# Using config file
|
||||
python eval_tools/eval.py --config eval_tools/configs/eval_config.yaml
|
||||
|
||||
# 2D evaluation only
|
||||
python eval_tools/eval.py --det-path /data/detections --gt-path /data/labels --eval-2d-only
|
||||
"""
|
||||
)
|
||||
|
||||
# Path arguments
|
||||
parser.add_argument('--det-path', type=str,
|
||||
help='Path to detection results root directory (contains case folders)')
|
||||
parser.add_argument('--gt-path', type=str,
|
||||
help='Path to ground truth labels root directory (contains case folders)')
|
||||
parser.add_argument('--path-depth', type=int, default=1, choices=[1, 2],
|
||||
help='Directory depth: 1 = det_path/case/txt_results, 2 = det_path/level1/case/txt_results (default: 1)')
|
||||
parser.add_argument('--det-format', type=str, default='auto', choices=['auto', 'json', 'txt'],
|
||||
help='Detection file format: auto (default, probe json_results/ then txt_results/), json, or txt')
|
||||
parser.add_argument('--gt-format', type=str, default='auto', choices=['auto', 'json', 'txt'],
|
||||
help='Ground truth file format: auto (default, probe labels_json/ then labels/), json, or txt')
|
||||
parser.add_argument('--output-dir', type=str, default='eval_results',
|
||||
help='Output directory for evaluation results (default: eval_results)')
|
||||
|
||||
# Config file
|
||||
parser.add_argument('--config', type=str,
|
||||
help='Path to YAML configuration file')
|
||||
|
||||
# Image parameters
|
||||
parser.add_argument('--img-width', type=int, default=1920,
|
||||
help='Image width in pixels (default: 1920)')
|
||||
parser.add_argument('--img-height', type=int, default=1080,
|
||||
help='Image height in pixels (default: 1080)')
|
||||
|
||||
# Evaluation options
|
||||
parser.add_argument('--eval-2d-only', action='store_true',
|
||||
help='Evaluate 2D detection only')
|
||||
parser.add_argument('--eval-3d-only', action='store_true',
|
||||
help='Evaluate 3D detection only')
|
||||
parser.add_argument('--iou-threshold', type=float, default=0.5,
|
||||
help='IoU threshold for 2D matching (default: 0.5)')
|
||||
parser.add_argument('--conf-threshold', type=float, default=0.5,
|
||||
help='Confidence threshold for precision/recall (default: 0.5)')
|
||||
parser.add_argument('--ap-method', type=str, default='voc2010',
|
||||
choices=['voc2010', 'coco'],
|
||||
help='AP calculation method (default: voc2010)')
|
||||
parser.add_argument('--heading-tolerance', type=str, default='strict',
|
||||
choices=['strict', 'relaxed', 'both'],
|
||||
help='Heading error calculation mode: strict (default), relaxed (180° symmetry), or both')
|
||||
parser.add_argument('--coord-system', type=str, default='camera',
|
||||
choices=['camera', 'ego'],
|
||||
help='3D evaluation coordinate system: camera (default) or ego')
|
||||
parser.add_argument('--num-workers', type=int, default=None,
|
||||
help='Number of parallel workers for evaluation (default: auto-detect)')
|
||||
parser.add_argument('--roi-bottom-offset', type=int, default=None,
|
||||
help='Pixels to trim from bottom edge of ROI (shifts y2 upward, default: from config or 0)')
|
||||
parser.add_argument('--save-detailed-matches', action='store_true',
|
||||
help='Save detailed 3D match information for common-match comparison')
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
"""Main evaluation function."""
|
||||
args = parse_arguments()
|
||||
|
||||
# Load configuration
|
||||
config = {}
|
||||
if args.config:
|
||||
print(f"Loading configuration from: {args.config}")
|
||||
config = load_config(args.config)
|
||||
|
||||
# Override with command line arguments if provided
|
||||
if args.det_path:
|
||||
config.setdefault('dataset', {})['det_path'] = args.det_path
|
||||
if args.gt_path:
|
||||
config.setdefault('dataset', {})['gt_path'] = args.gt_path
|
||||
if args.path_depth != 1:
|
||||
config.setdefault('dataset', {})['path_depth'] = args.path_depth
|
||||
if args.det_format != 'auto':
|
||||
config.setdefault('dataset', {})['det_format'] = args.det_format
|
||||
if args.gt_format != 'auto':
|
||||
config.setdefault('dataset', {})['gt_format'] = args.gt_format
|
||||
# Only override output_dir if explicitly provided (not default value)
|
||||
if args.output_dir != 'eval_results':
|
||||
config.setdefault('output', {})['save_path'] = args.output_dir
|
||||
|
||||
# Override image size if explicitly provided
|
||||
if args.img_width != 1920:
|
||||
config.setdefault('image', {})['width'] = args.img_width
|
||||
if args.img_height != 1080:
|
||||
config.setdefault('image', {})['height'] = args.img_height
|
||||
|
||||
# Override matching threshold if explicitly provided
|
||||
if args.iou_threshold != 0.5:
|
||||
config.setdefault('matching', {})['iou_threshold'] = args.iou_threshold
|
||||
|
||||
# Override 2D metrics parameters if explicitly provided
|
||||
if args.conf_threshold != 0.5:
|
||||
config.setdefault('metrics_2d', {})['conf_threshold'] = args.conf_threshold
|
||||
if args.ap_method != 'voc2010':
|
||||
config.setdefault('metrics_2d', {})['ap_method'] = args.ap_method
|
||||
|
||||
# Override 3D metrics parameters if explicitly provided
|
||||
if args.heading_tolerance != 'strict':
|
||||
config.setdefault('metrics_3d', {})['heading_tolerance'] = args.heading_tolerance
|
||||
if args.coord_system != 'camera':
|
||||
config.setdefault('metrics_3d', {})['coordinate_system'] = args.coord_system
|
||||
|
||||
# Override roi_bottom_offset if explicitly provided
|
||||
if args.roi_bottom_offset is not None:
|
||||
config.setdefault('roi_gt', {})['roi_bottom_offset'] = args.roi_bottom_offset
|
||||
else:
|
||||
# Build config from command line arguments
|
||||
if not args.det_path or not args.gt_path:
|
||||
print("Error: --det-path and --gt-path are required when not using --config")
|
||||
sys.exit(1)
|
||||
|
||||
config = {
|
||||
'dataset': {
|
||||
'det_path': args.det_path,
|
||||
'gt_path': args.gt_path,
|
||||
'path_depth': args.path_depth,
|
||||
'det_format': args.det_format,
|
||||
'gt_format': args.gt_format,
|
||||
},
|
||||
'image': {
|
||||
'width': args.img_width,
|
||||
'height': args.img_height
|
||||
},
|
||||
'matching': {
|
||||
'iou_threshold': args.iou_threshold
|
||||
},
|
||||
'metrics_2d': {
|
||||
'enabled': not args.eval_3d_only,
|
||||
'conf_threshold': args.conf_threshold,
|
||||
'ap_method': args.ap_method
|
||||
},
|
||||
'metrics_3d': {
|
||||
'enabled': not args.eval_2d_only,
|
||||
'heading_tolerance': args.heading_tolerance,
|
||||
'coordinate_system': args.coord_system,
|
||||
},
|
||||
'output': {
|
||||
'save_path': args.output_dir
|
||||
}
|
||||
}
|
||||
|
||||
if args.roi_bottom_offset is not None:
|
||||
config.setdefault('roi_gt', {})['roi_bottom_offset'] = args.roi_bottom_offset
|
||||
|
||||
# Set evaluation flags
|
||||
config['eval_2d'] = config.get('metrics_2d', {}).get('enabled', True)
|
||||
config['eval_3d'] = config.get('metrics_3d', {}).get('enabled', True)
|
||||
|
||||
if args.eval_2d_only:
|
||||
config['eval_2d'] = True
|
||||
config['eval_3d'] = False
|
||||
elif args.eval_3d_only:
|
||||
config['eval_2d'] = False
|
||||
config['eval_3d'] = True
|
||||
|
||||
# Extract paths
|
||||
det_path = config['dataset']['det_path']
|
||||
gt_path = config['dataset']['gt_path']
|
||||
path_depth = config['dataset'].get('path_depth', 1) # Default to 1-level structure
|
||||
det_format = config['dataset'].get('det_format', 'auto')
|
||||
gt_format = config['dataset'].get('gt_format', 'auto')
|
||||
det_subdir = config['dataset'].get('det_subdir')
|
||||
output_dir = config['output']['save_path']
|
||||
|
||||
img_width = config.get('image', {}).get('width', 1920)
|
||||
img_height = config.get('image', {}).get('height', 1080)
|
||||
iou_threshold = config.get('matching', {}).get('iou_threshold', 0.5)
|
||||
|
||||
# Get num_workers from config if not specified in command line
|
||||
num_workers = args.num_workers
|
||||
if num_workers is None and 'performance' in config:
|
||||
num_workers = config['performance'].get('num_workers', None)
|
||||
|
||||
print("="*80)
|
||||
print("YOLOv5-3D Model Evaluation")
|
||||
print("="*80)
|
||||
print(f"Detection path: {det_path}")
|
||||
print(f"Ground truth path: {gt_path}")
|
||||
print(f"Path depth: {path_depth}")
|
||||
print(f"Detection format: {det_format}")
|
||||
print(f"Ground truth format: {gt_format}")
|
||||
if det_subdir:
|
||||
print(f"Detection subdirectory: {det_subdir}")
|
||||
print(f"Output directory: {output_dir}")
|
||||
print(f"Image size: {img_width}x{img_height}")
|
||||
print(f"IoU threshold: {iou_threshold}")
|
||||
print(f"Confidence threshold: {config.get('metrics_2d', {}).get('conf_threshold', 0.5)}")
|
||||
print(f"AP method: {config.get('metrics_2d', {}).get('ap_method', 'voc2010')}")
|
||||
heading_tolerance = config.get('metrics_3d', {}).get('heading_tolerance', 'strict')
|
||||
coord_system = config.get('metrics_3d', {}).get('coordinate_system', 'camera')
|
||||
print(f"Heading tolerance: {heading_tolerance}")
|
||||
print(f"3D coordinate system: {coord_system}")
|
||||
roi_bottom_offset_val = config.get('roi_gt', {}).get('roi_bottom_offset', 0)
|
||||
if config.get('roi_gt', {}).get('enabled', False):
|
||||
print(f"ROI bottom offset: {roi_bottom_offset_val}")
|
||||
if num_workers:
|
||||
print(f"Number of workers: {num_workers}")
|
||||
print(f"Evaluate 2D: {config['eval_2d']}")
|
||||
print(f"Evaluate 3D: {config['eval_3d']}")
|
||||
if args.save_detailed_matches:
|
||||
print(f"Save detailed matches: Yes")
|
||||
print("="*80)
|
||||
|
||||
# Initialize evaluator
|
||||
evaluator = Evaluator(
|
||||
config=config,
|
||||
iou_threshold=iou_threshold,
|
||||
num_workers=num_workers,
|
||||
save_detailed_matches=args.save_detailed_matches
|
||||
)
|
||||
|
||||
# Load data
|
||||
print("\nLoading data...")
|
||||
evaluator.load_data_from_paths(det_path, gt_path, img_width, img_height, path_depth,
|
||||
det_format=det_format, gt_format=gt_format)
|
||||
|
||||
if len(evaluator.image_pairs) == 0:
|
||||
print("Error: No image pairs found for evaluation!")
|
||||
sys.exit(1)
|
||||
|
||||
# Run evaluation
|
||||
results = evaluator.evaluate()
|
||||
|
||||
# Generate and save report
|
||||
evaluator.generate_report(results, output_dir)
|
||||
|
||||
print("\n✓ Evaluation completed successfully!")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
584
eval_tools/core/rename_detection_frames.py
Executable file
584
eval_tools/core/rename_detection_frames.py
Executable file
@@ -0,0 +1,584 @@
|
||||
"""Rename detection result files so frame stems match ground-truth stems.
|
||||
|
||||
This script is intended as a preprocessing step before running the evaluation
|
||||
pipeline. The evaluator pairs detection and ground-truth files strictly by
|
||||
their filename stem, so detections that omit a timestamp suffix will be skipped
|
||||
unless they are renamed to the GT stem.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
from collections import defaultdict
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
DEFAULT_TIMESTAMP_PATTERNS = (
|
||||
re.compile(r"(?:[_-]?ts)?\d{10,20}$"),
|
||||
re.compile(r"[_-]?\d{8}[_-]\d{6}(?:\d+)?$"),
|
||||
re.compile(r"[_-]?\d{8}T\d{6}(?:\d+)?$"),
|
||||
)
|
||||
DEFAULT_TIMESTAMP_TOKEN_PATTERN = re.compile(r"^(?:ts)?\d{8,20}$")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CaseInfo:
|
||||
"""One evaluation case directory."""
|
||||
|
||||
det_case_dir: Path
|
||||
case_name: str
|
||||
level1_name: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MatchPair:
|
||||
"""Mapping from one detection file to one GT file."""
|
||||
|
||||
det_file: Path
|
||||
gt_file: Path
|
||||
strategy: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CaseProcessResult:
|
||||
"""Worker result for one case."""
|
||||
|
||||
summary: dict[str, object]
|
||||
manifest_rows: list[dict[str, object]]
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
"""Parse command-line arguments."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Rename or copy detection result files so their stems match GT stems "
|
||||
"before running eval_tools/core/eval.py."
|
||||
)
|
||||
)
|
||||
parser.add_argument("--config", type=Path, help="Evaluation config YAML used to infer det/gt paths.")
|
||||
parser.add_argument("--det-root", type=Path, help="Detection root directory. Overrides config dataset.det_path.")
|
||||
parser.add_argument("--gt-root", type=Path, help="Ground-truth root directory. Overrides config dataset.gt_path.")
|
||||
parser.add_argument(
|
||||
"--path-depth",
|
||||
type=int,
|
||||
choices=(1, 2),
|
||||
help="Directory depth: 1 = root/case, 2 = root/level1/case. Overrides config dataset.path_depth.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--det-format",
|
||||
choices=("auto", "json", "txt"),
|
||||
help="Detection format. Overrides config dataset.det_format.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--gt-format",
|
||||
choices=("auto", "json", "txt"),
|
||||
help="GT format. Overrides config dataset.gt_format.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--det-subdir",
|
||||
help="Relative detection subdirectory inside each case, e.g. predictions/roi0.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-root",
|
||||
type=Path,
|
||||
help=(
|
||||
"Write renamed files to a mirrored directory tree instead of changing the source det root in place. "
|
||||
"Recommended for the first run."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--in-place",
|
||||
action="store_true",
|
||||
help="Rename files directly inside the source detection directory.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--enable-index-fallback",
|
||||
action="store_true",
|
||||
help=(
|
||||
"If normalized-name matching still leaves unmatched files, allow one-to-one pairing by sorted index. "
|
||||
"Use only when detection and GT frame order is guaranteed to be aligned."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--strip-pattern",
|
||||
action="append",
|
||||
default=[],
|
||||
help=(
|
||||
"Custom regex used to strip a suffix from stems before matching. Can be specified multiple times. "
|
||||
"If omitted, built-in timestamp suffix patterns are used."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--case",
|
||||
dest="cases",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Limit processing to specific case names. Can be specified multiple times.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--manifest-path",
|
||||
type=Path,
|
||||
help="Where to save the rename manifest JSON. Defaults under output_root or det_root.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--strict",
|
||||
action="store_true",
|
||||
help="Return a non-zero exit code if any case has unmatched or ambiguous files.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Preview the mapping without copying or renaming files.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--num-workers",
|
||||
type=int,
|
||||
default=1,
|
||||
help="Number of case-level worker processes. Use 0 to auto-select based on CPU count.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def load_config(config_path: Path | None) -> dict:
|
||||
"""Load evaluation YAML config if provided."""
|
||||
if config_path is None:
|
||||
return {}
|
||||
with config_path.open("r", encoding="utf-8") as handle:
|
||||
return yaml.safe_load(handle) or {}
|
||||
|
||||
|
||||
def resolve_runtime_options(args: argparse.Namespace, config: dict) -> dict:
|
||||
"""Resolve CLI options with config fallback."""
|
||||
dataset_cfg = config.get("dataset", {}) if isinstance(config, dict) else {}
|
||||
|
||||
det_root = Path(args.det_root or dataset_cfg.get("det_path", "")).expanduser() if (args.det_root or dataset_cfg.get("det_path")) else None
|
||||
gt_root = Path(args.gt_root or dataset_cfg.get("gt_path", "")).expanduser() if (args.gt_root or dataset_cfg.get("gt_path")) else None
|
||||
path_depth = args.path_depth or dataset_cfg.get("path_depth", 1)
|
||||
det_format = args.det_format or dataset_cfg.get("det_format", "auto")
|
||||
gt_format = args.gt_format or dataset_cfg.get("gt_format", "auto")
|
||||
det_subdir = args.det_subdir if args.det_subdir is not None else dataset_cfg.get("det_subdir")
|
||||
|
||||
if det_root is None or gt_root is None:
|
||||
raise ValueError("Both detection root and GT root must be provided by --config or explicit arguments.")
|
||||
if args.in_place and args.output_root:
|
||||
raise ValueError("Use either --in-place or --output-root, not both.")
|
||||
if not args.in_place and args.output_root is None and not args.dry_run:
|
||||
raise ValueError("Choose --output-root for a copied result tree or --in-place for source renaming.")
|
||||
|
||||
return {
|
||||
"det_root": det_root.resolve(),
|
||||
"gt_root": gt_root.resolve(),
|
||||
"path_depth": int(path_depth),
|
||||
"det_format": det_format,
|
||||
"gt_format": gt_format,
|
||||
"det_subdir": det_subdir,
|
||||
}
|
||||
|
||||
|
||||
def iter_case_dirs(det_root: Path, path_depth: int, selected_cases: set[str]) -> list[CaseInfo]:
|
||||
"""Enumerate detection case directories according to evaluator path depth."""
|
||||
def is_case_dir(path: Path) -> bool:
|
||||
return path.is_dir() and not path.name.startswith((".", "_"))
|
||||
|
||||
cases: list[CaseInfo] = []
|
||||
if path_depth == 1:
|
||||
for case_dir in sorted(p for p in det_root.iterdir() if is_case_dir(p)):
|
||||
if selected_cases and case_dir.name not in selected_cases:
|
||||
continue
|
||||
cases.append(CaseInfo(det_case_dir=case_dir, case_name=case_dir.name))
|
||||
return cases
|
||||
|
||||
for level1_dir in sorted(p for p in det_root.iterdir() if is_case_dir(p)):
|
||||
for case_dir in sorted(p for p in level1_dir.iterdir() if is_case_dir(p)):
|
||||
if selected_cases and case_dir.name not in selected_cases:
|
||||
continue
|
||||
cases.append(CaseInfo(det_case_dir=case_dir, case_name=case_dir.name, level1_name=level1_dir.name))
|
||||
return cases
|
||||
|
||||
|
||||
def resolve_gt_case_dir(gt_root: Path, case_info: CaseInfo, path_depth: int) -> Path:
|
||||
"""Resolve ground-truth case directory from case info."""
|
||||
if path_depth == 1:
|
||||
return gt_root / case_info.case_name
|
||||
return gt_root / str(case_info.level1_name) / case_info.case_name
|
||||
|
||||
|
||||
def resolve_detection_dir(case_dir: Path, det_format: str, det_subdir: str | None) -> tuple[Path | None, str | None]:
|
||||
"""Resolve the directory and suffix for detection result files."""
|
||||
json_candidates: list[Path] = []
|
||||
if det_subdir:
|
||||
subdir_path = Path(det_subdir)
|
||||
json_candidates.append(subdir_path if subdir_path.is_absolute() else case_dir / subdir_path)
|
||||
json_candidates.extend([case_dir / "json_results", case_dir / "predictions"])
|
||||
txt_candidate = case_dir / "txt_results"
|
||||
|
||||
def pick_json_dir() -> Path | None:
|
||||
for candidate in json_candidates:
|
||||
if candidate.exists() and any(candidate.glob("*.json")):
|
||||
return candidate
|
||||
return None
|
||||
|
||||
if det_format == "json":
|
||||
result_dir = pick_json_dir()
|
||||
return result_dir, ".json"
|
||||
if det_format == "txt":
|
||||
return (txt_candidate, ".txt") if txt_candidate.exists() else (None, None)
|
||||
|
||||
result_dir = pick_json_dir()
|
||||
if result_dir is not None:
|
||||
return result_dir, ".json"
|
||||
if txt_candidate.exists() and any(txt_candidate.glob("*.txt")):
|
||||
return txt_candidate, ".txt"
|
||||
return None, None
|
||||
|
||||
|
||||
def resolve_gt_dir(case_dir: Path, gt_format: str) -> tuple[Path | None, str | None]:
|
||||
"""Resolve the directory and suffix for GT files."""
|
||||
json_candidate = case_dir / "labels_json"
|
||||
txt_candidate = case_dir / "labels"
|
||||
|
||||
if gt_format == "json":
|
||||
return (json_candidate, ".json") if json_candidate.exists() else (None, None)
|
||||
if gt_format == "txt":
|
||||
return (txt_candidate, ".txt") if txt_candidate.exists() else (None, None)
|
||||
if json_candidate.exists():
|
||||
return json_candidate, ".json"
|
||||
if txt_candidate.exists():
|
||||
return txt_candidate, ".txt"
|
||||
return None, None
|
||||
|
||||
|
||||
def build_normalizer(custom_patterns: list[str]) -> Callable[[str], str]:
|
||||
"""Build a stem normalizer that removes timestamp-like suffixes."""
|
||||
patterns = [re.compile(pattern) for pattern in custom_patterns] if custom_patterns else list(DEFAULT_TIMESTAMP_PATTERNS)
|
||||
|
||||
def normalize(stem: str) -> str:
|
||||
normalized = stem
|
||||
for pattern in patterns:
|
||||
normalized = pattern.sub("", normalized)
|
||||
tokens = [token for token in normalized.rstrip("_-").split("_") if token]
|
||||
filtered_tokens = [token for token in tokens if not DEFAULT_TIMESTAMP_TOKEN_PATTERN.fullmatch(token)]
|
||||
return "_".join(filtered_tokens)
|
||||
|
||||
return normalize
|
||||
|
||||
|
||||
def build_pairs(
|
||||
det_files: list[Path],
|
||||
gt_files: list[Path],
|
||||
normalize_stem: Callable[[str], str],
|
||||
enable_index_fallback: bool,
|
||||
) -> tuple[list[MatchPair], list[str], list[dict[str, object]]]:
|
||||
"""Match detection files to GT files using exact, normalized, then optional index fallback."""
|
||||
gt_by_stem = {path.stem: path for path in gt_files}
|
||||
used_gt_stems: set[str] = set()
|
||||
pairs: list[MatchPair] = []
|
||||
issues: list[str] = []
|
||||
ambiguous_groups: list[dict[str, object]] = []
|
||||
|
||||
unmatched_det: list[Path] = []
|
||||
for det_file in det_files:
|
||||
gt_file = gt_by_stem.get(det_file.stem)
|
||||
if gt_file is None:
|
||||
unmatched_det.append(det_file)
|
||||
continue
|
||||
used_gt_stems.add(gt_file.stem)
|
||||
pairs.append(MatchPair(det_file=det_file, gt_file=gt_file, strategy="exact"))
|
||||
|
||||
unmatched_gt = [gt_file for gt_file in gt_files if gt_file.stem not in used_gt_stems]
|
||||
if not unmatched_det or not unmatched_gt:
|
||||
return pairs, issues, ambiguous_groups
|
||||
|
||||
det_groups: dict[str, list[Path]] = defaultdict(list)
|
||||
gt_groups: dict[str, list[Path]] = defaultdict(list)
|
||||
for det_file in unmatched_det:
|
||||
det_groups[normalize_stem(det_file.stem)].append(det_file)
|
||||
for gt_file in unmatched_gt:
|
||||
gt_groups[normalize_stem(gt_file.stem)].append(gt_file)
|
||||
|
||||
remaining_det: list[Path] = []
|
||||
remaining_gt: list[Path] = []
|
||||
group_keys = sorted(set(det_groups) | set(gt_groups))
|
||||
for key in group_keys:
|
||||
det_group = sorted(det_groups.get(key, []))
|
||||
gt_group = sorted(gt_groups.get(key, []))
|
||||
if not det_group:
|
||||
remaining_gt.extend(gt_group)
|
||||
continue
|
||||
if not gt_group:
|
||||
remaining_det.extend(det_group)
|
||||
continue
|
||||
if len(det_group) == 1 and len(gt_group) == 1:
|
||||
pairs.append(MatchPair(det_file=det_group[0], gt_file=gt_group[0], strategy="normalized"))
|
||||
continue
|
||||
|
||||
ambiguous_groups.append(
|
||||
{
|
||||
"normalized_key": key,
|
||||
"det_files": [path.name for path in det_group],
|
||||
"gt_files": [path.name for path in gt_group],
|
||||
}
|
||||
)
|
||||
remaining_det.extend(det_group)
|
||||
remaining_gt.extend(gt_group)
|
||||
|
||||
if remaining_det or remaining_gt:
|
||||
if enable_index_fallback and len(remaining_det) == len(remaining_gt):
|
||||
for det_file, gt_file in zip(sorted(remaining_det), sorted(remaining_gt)):
|
||||
pairs.append(MatchPair(det_file=det_file, gt_file=gt_file, strategy="index"))
|
||||
else:
|
||||
if remaining_det:
|
||||
issues.append(
|
||||
"Unmatched detection files: " + ", ".join(path.name for path in sorted(remaining_det))
|
||||
)
|
||||
if remaining_gt:
|
||||
issues.append(
|
||||
"Unused GT files: " + ", ".join(path.name for path in sorted(remaining_gt))
|
||||
)
|
||||
|
||||
return pairs, issues, ambiguous_groups
|
||||
|
||||
|
||||
def ensure_parent(path: Path) -> None:
|
||||
"""Create parent directory if missing."""
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def apply_mapping(
|
||||
pairs: list[MatchPair],
|
||||
det_root: Path,
|
||||
output_root: Path | None,
|
||||
in_place: bool,
|
||||
dry_run: bool,
|
||||
) -> list[dict[str, object]]:
|
||||
"""Apply renaming or copying for matched pairs."""
|
||||
manifest_rows: list[dict[str, object]] = []
|
||||
|
||||
for pair in pairs:
|
||||
relative_det_path = pair.det_file.relative_to(det_root)
|
||||
target_name = f"{pair.gt_file.stem}{pair.det_file.suffix}"
|
||||
if output_root is not None:
|
||||
target_dir = output_root / relative_det_path.parent
|
||||
target_path = target_dir / target_name
|
||||
action = "copy"
|
||||
else:
|
||||
target_path = pair.det_file.with_name(target_name)
|
||||
action = "rename"
|
||||
|
||||
row = {
|
||||
"strategy": pair.strategy,
|
||||
"action": action,
|
||||
"source": str(pair.det_file),
|
||||
"target": str(target_path),
|
||||
"gt_file": str(pair.gt_file),
|
||||
"changed": pair.det_file.name != target_path.name,
|
||||
}
|
||||
manifest_rows.append(row)
|
||||
|
||||
if dry_run:
|
||||
continue
|
||||
|
||||
ensure_parent(target_path)
|
||||
if output_root is not None:
|
||||
if pair.det_file.resolve() == target_path.resolve():
|
||||
continue
|
||||
shutil.copy2(pair.det_file, target_path)
|
||||
elif in_place and pair.det_file != target_path:
|
||||
if target_path.exists():
|
||||
raise FileExistsError(f"Refusing to overwrite existing file: {target_path}")
|
||||
pair.det_file.rename(target_path)
|
||||
|
||||
return manifest_rows
|
||||
|
||||
|
||||
def default_manifest_path(args: argparse.Namespace, runtime: dict) -> Path:
|
||||
"""Choose a default manifest path."""
|
||||
if args.manifest_path is not None:
|
||||
return args.manifest_path
|
||||
root = args.output_root.resolve() if args.output_root else runtime["det_root"]
|
||||
return root / "rename_detection_frames_manifest.json"
|
||||
|
||||
|
||||
def resolve_num_workers(requested_workers: int, case_count: int) -> int:
|
||||
"""Resolve worker count from CLI input and case count."""
|
||||
if case_count <= 1:
|
||||
return 1
|
||||
if requested_workers <= 0:
|
||||
requested_workers = os.cpu_count() or 1
|
||||
return max(1, min(requested_workers, case_count))
|
||||
|
||||
|
||||
def process_case(
|
||||
case_info: CaseInfo,
|
||||
runtime: dict[str, object],
|
||||
strip_patterns: list[str],
|
||||
enable_index_fallback: bool,
|
||||
output_root: str | None,
|
||||
in_place: bool,
|
||||
dry_run: bool,
|
||||
) -> CaseProcessResult:
|
||||
"""Process one case, including match generation and optional file operations."""
|
||||
normalize_stem = build_normalizer(strip_patterns)
|
||||
det_root = Path(str(runtime["det_root"]))
|
||||
gt_root = Path(str(runtime["gt_root"]))
|
||||
path_depth = int(runtime["path_depth"])
|
||||
det_format = str(runtime["det_format"])
|
||||
gt_format = str(runtime["gt_format"])
|
||||
det_subdir = runtime.get("det_subdir")
|
||||
det_subdir_str = None if det_subdir is None else str(det_subdir)
|
||||
|
||||
gt_case_dir = resolve_gt_case_dir(gt_root, case_info, path_depth)
|
||||
det_dir, det_suffix = resolve_detection_dir(case_info.det_case_dir, det_format, det_subdir_str)
|
||||
gt_dir, gt_suffix = resolve_gt_dir(gt_case_dir, gt_format)
|
||||
|
||||
summary = {
|
||||
"case": case_info.case_name,
|
||||
"level1_name": case_info.level1_name,
|
||||
"det_case_dir": str(case_info.det_case_dir),
|
||||
"gt_case_dir": str(gt_case_dir),
|
||||
"status": "ok",
|
||||
"issues": [],
|
||||
"ambiguous_groups": [],
|
||||
"matched": 0,
|
||||
"changed": 0,
|
||||
}
|
||||
|
||||
if det_dir is None or det_suffix is None:
|
||||
summary["status"] = "missing_detection_dir"
|
||||
summary["issues"].append("Detection results directory not found or empty.")
|
||||
return CaseProcessResult(summary=summary, manifest_rows=[])
|
||||
if gt_dir is None or gt_suffix is None:
|
||||
summary["status"] = "missing_gt_dir"
|
||||
summary["issues"].append("GT labels directory not found.")
|
||||
return CaseProcessResult(summary=summary, manifest_rows=[])
|
||||
|
||||
det_files = sorted(path for path in det_dir.iterdir() if path.suffix == det_suffix)
|
||||
gt_files = sorted(path for path in gt_dir.iterdir() if path.suffix == gt_suffix)
|
||||
if not det_files:
|
||||
summary["status"] = "empty_detection_dir"
|
||||
summary["issues"].append("No detection files found in resolved directory.")
|
||||
return CaseProcessResult(summary=summary, manifest_rows=[])
|
||||
if not gt_files:
|
||||
summary["status"] = "empty_gt_dir"
|
||||
summary["issues"].append("No GT files found in resolved directory.")
|
||||
return CaseProcessResult(summary=summary, manifest_rows=[])
|
||||
|
||||
pairs, issues, ambiguous_groups = build_pairs(
|
||||
det_files=det_files,
|
||||
gt_files=gt_files,
|
||||
normalize_stem=normalize_stem,
|
||||
enable_index_fallback=enable_index_fallback,
|
||||
)
|
||||
manifest_rows = apply_mapping(
|
||||
pairs=pairs,
|
||||
det_root=det_root,
|
||||
output_root=Path(output_root) if output_root else None,
|
||||
in_place=in_place,
|
||||
dry_run=dry_run,
|
||||
)
|
||||
|
||||
summary["issues"] = issues
|
||||
summary["ambiguous_groups"] = ambiguous_groups
|
||||
summary["matched"] = len(pairs)
|
||||
summary["changed"] = sum(1 for row in manifest_rows if row["changed"])
|
||||
if issues or ambiguous_groups:
|
||||
summary["status"] = "partial"
|
||||
return CaseProcessResult(summary=summary, manifest_rows=manifest_rows)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Run the rename workflow."""
|
||||
args = parse_args()
|
||||
config = load_config(args.config)
|
||||
runtime = resolve_runtime_options(args, config)
|
||||
selected_cases = set(args.cases)
|
||||
|
||||
case_infos = iter_case_dirs(runtime["det_root"], runtime["path_depth"], selected_cases)
|
||||
if not case_infos:
|
||||
raise FileNotFoundError(f"No detection cases found under {runtime['det_root']}")
|
||||
num_workers = resolve_num_workers(args.num_workers, len(case_infos))
|
||||
|
||||
all_manifest_rows: list[dict[str, object]] = []
|
||||
case_summaries: list[dict[str, object]] = []
|
||||
failed_cases = 0
|
||||
|
||||
output_root = str(args.output_root.resolve()) if args.output_root else None
|
||||
if num_workers == 1:
|
||||
results = [
|
||||
process_case(
|
||||
case_info=case_info,
|
||||
runtime=runtime,
|
||||
strip_patterns=args.strip_pattern,
|
||||
enable_index_fallback=args.enable_index_fallback,
|
||||
output_root=output_root,
|
||||
in_place=args.in_place,
|
||||
dry_run=args.dry_run,
|
||||
)
|
||||
for case_info in case_infos
|
||||
]
|
||||
else:
|
||||
with ProcessPoolExecutor(max_workers=num_workers) as executor:
|
||||
results = list(
|
||||
executor.map(
|
||||
process_case,
|
||||
case_infos,
|
||||
[runtime] * len(case_infos),
|
||||
[args.strip_pattern] * len(case_infos),
|
||||
[args.enable_index_fallback] * len(case_infos),
|
||||
[output_root] * len(case_infos),
|
||||
[args.in_place] * len(case_infos),
|
||||
[args.dry_run] * len(case_infos),
|
||||
)
|
||||
)
|
||||
|
||||
for result in results:
|
||||
case_summaries.append(result.summary)
|
||||
all_manifest_rows.extend(result.manifest_rows)
|
||||
if result.summary["status"] != "ok":
|
||||
failed_cases += 1
|
||||
|
||||
manifest = {
|
||||
"dry_run": args.dry_run,
|
||||
"in_place": args.in_place,
|
||||
"output_root": str(args.output_root.resolve()) if args.output_root else None,
|
||||
"det_root": str(runtime["det_root"]),
|
||||
"gt_root": str(runtime["gt_root"]),
|
||||
"path_depth": runtime["path_depth"],
|
||||
"det_format": runtime["det_format"],
|
||||
"gt_format": runtime["gt_format"],
|
||||
"det_subdir": runtime["det_subdir"],
|
||||
"cases": case_summaries,
|
||||
"mappings": all_manifest_rows,
|
||||
}
|
||||
|
||||
manifest_path = default_manifest_path(args, runtime)
|
||||
ensure_parent(manifest_path)
|
||||
with manifest_path.open("w", encoding="utf-8") as handle:
|
||||
json.dump(manifest, handle, ensure_ascii=False, indent=2)
|
||||
|
||||
total_cases = len(case_summaries)
|
||||
total_matched = sum(item["matched"] for item in case_summaries)
|
||||
total_changed = sum(item["changed"] for item in case_summaries)
|
||||
print(f"Processed cases: {total_cases}")
|
||||
print(f"Workers used: {num_workers}")
|
||||
print(f"Matched files: {total_matched}")
|
||||
print(f"Renamed/copied files with changed stems: {total_changed}")
|
||||
print(f"Manifest: {manifest_path}")
|
||||
|
||||
if args.strict and failed_cases:
|
||||
print(f"Strict mode: {failed_cases} case(s) still have unresolved issues.")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
79
eval_tools/core/run_eval.sh
Executable file
79
eval_tools/core/run_eval.sh
Executable file
@@ -0,0 +1,79 @@
|
||||
#!/bin/bash
|
||||
|
||||
# YOLOv5-3D 评测启动脚本
|
||||
# 使用方法: sh eval_tools/run_eval.sh
|
||||
|
||||
# ==============================================
|
||||
# 配置参数
|
||||
# ==============================================
|
||||
|
||||
# 数据路径
|
||||
|
||||
# Set PYTHONPATH to project root for module imports
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
export PYTHONPATH="${PROJECT_ROOT}:${PYTHONPATH}"
|
||||
DET_PATH="/data1/dongying/Mono3d/G1M3/300w/txt_results"
|
||||
|
||||
GT_PATH="/data1/dongying/Mono3d/G1M3/labels"
|
||||
OUTPUT_DIR="eval_results/$(date +%Y%m%d_%H%M%S)"
|
||||
|
||||
# 图像参数
|
||||
IMG_WIDTH=1920
|
||||
IMG_HEIGHT=1080
|
||||
|
||||
# 评测参数
|
||||
IOU_THRESHOLD=0.5
|
||||
CONF_THRESHOLD=0.5
|
||||
AP_METHOD="voc2010" # voc2010 或 coco
|
||||
|
||||
# 并行进程数 (默认自动检测)
|
||||
NUM_WORKERS=8 # 设置为空使用自动检测: NUM_WORKERS=""
|
||||
|
||||
# ==============================================
|
||||
# 构建命令
|
||||
# ==============================================
|
||||
# 构建命令
|
||||
# ==============================================
|
||||
|
||||
CMD="python eval_tools/core/eval.py \
|
||||
--det-path $DET_PATH \
|
||||
--gt-path $GT_PATH \
|
||||
--output-dir $OUTPUT_DIR \
|
||||
--img-width $IMG_WIDTH \
|
||||
--img-height $IMG_HEIGHT \
|
||||
--iou-threshold $IOU_THRESHOLD \
|
||||
--conf-threshold $CONF_THRESHOLD \
|
||||
--ap-method $AP_METHOD"
|
||||
|
||||
# 添加并行进程数参数
|
||||
if [ -n "$NUM_WORKERS" ]; then
|
||||
CMD="$CMD --num-workers $NUM_WORKERS"
|
||||
fi
|
||||
|
||||
# ==============================================
|
||||
# 执行评测
|
||||
# ==============================================
|
||||
|
||||
echo "========================================"
|
||||
echo "YOLOv5-3D 评测"
|
||||
echo "========================================"
|
||||
echo "检测结果: $DET_PATH"
|
||||
echo "真值标签: $GT_PATH"
|
||||
echo "输出目录: $OUTPUT_DIR"
|
||||
echo "图像尺寸: ${IMG_WIDTH}x${IMG_HEIGHT}"
|
||||
echo "IoU阈值: $IOU_THRESHOLD"
|
||||
echo "置信度阈值: $CONF_THRESHOLD"
|
||||
if [ -n "$NUM_WORKERS" ]; then
|
||||
echo "并行进程: $NUM_WORKERS"
|
||||
else
|
||||
echo "并行进程: 自动检测"
|
||||
fi
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# 执行命令
|
||||
eval $CMD
|
||||
|
||||
echo ""
|
||||
echo "评测完成! 结果已保存到: $OUTPUT_DIR"
|
||||
25
eval_tools/core/run_eval_2d_only.sh
Executable file
25
eval_tools/core/run_eval_2d_only.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
# YOLOv5-3D 快速评测脚本 - 仅评测2D
|
||||
# 使用方法: sh eval_tools/run_eval_2d_only.sh
|
||||
|
||||
|
||||
# Set PYTHONPATH to project root for module imports
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
export PYTHONPATH="${PROJECT_ROOT}:${PYTHONPATH}"
|
||||
DET_PATH="/data1/dongying/Mono3d/G1M3/300w/txt_results"
|
||||
|
||||
GT_PATH="/data1/dongying/Mono3d/G1M3/labels"
|
||||
OUTPUT_DIR="eval_results/2d_only_$(date +%Y%m%d_%H%M%S)"
|
||||
|
||||
python eval_tools/core/eval.py \
|
||||
--det-path $DET_PATH \
|
||||
--gt-path $GT_PATH \
|
||||
--output-dir $OUTPUT_DIR \
|
||||
--eval-2d-only \
|
||||
--num-workers 8 \
|
||||
--img-width 1920 \
|
||||
--img-height 1080
|
||||
|
||||
echo "2D评测完成! 结果: $OUTPUT_DIR"
|
||||
25
eval_tools/core/run_eval_3d_only.sh
Executable file
25
eval_tools/core/run_eval_3d_only.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
# YOLOv5-3D 快速评测脚本 - 仅评测3D
|
||||
# 使用方法: sh eval_tools/run_eval_3d_only.sh
|
||||
|
||||
|
||||
# Set PYTHONPATH to project root for module imports
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
export PYTHONPATH="${PROJECT_ROOT}:${PYTHONPATH}"
|
||||
DET_PATH="/data1/dongying/Mono3d/G1M3/300w/txt_results"
|
||||
|
||||
GT_PATH="/data1/dongying/Mono3d/G1M3/labels"
|
||||
OUTPUT_DIR="eval_results/3d_only_$(date +%Y%m%d_%H%M%S)"
|
||||
|
||||
python eval_tools/core/eval.py \
|
||||
--det-path $DET_PATH \
|
||||
--gt-path $GT_PATH \
|
||||
--output-dir $OUTPUT_DIR \
|
||||
--eval-3d-only \
|
||||
--num-workers 8 \
|
||||
--img-width 1920 \
|
||||
--img-height 1080
|
||||
|
||||
echo "3D评测完成! 结果: $OUTPUT_DIR"
|
||||
84
eval_tools/core/run_eval_with_config.sh
Executable file
84
eval_tools/core/run_eval_with_config.sh
Executable file
@@ -0,0 +1,84 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Set PYTHONPATH to project root for module imports
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
export PYTHONPATH="${PROJECT_ROOT}:${PYTHONPATH}"
|
||||
|
||||
echo "================================================================================"
|
||||
echo "Evaluation Workflow"
|
||||
echo "================================================================================"
|
||||
|
||||
# Configuration
|
||||
MODEL_CONFIG_ROI0="eval_tools/configs/eval_config_yolov26s-roi0.yaml"
|
||||
MODEL_CONFIG_ROI1="eval_tools/configs/eval_config_yolov26s-roi1.yaml"
|
||||
OUTPUT_BASE="evaluation_results/eval_results_yolo26s_768_20260407_DL_KPI_SCENE" # Base directory for all evaluation outputs; individual runs will be timestamped subdirectories
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
|
||||
MODEL_NAME="yolo26s-20260407-conf0.27" # Used for output directory naming
|
||||
|
||||
# Heading tolerance mode: strict, relaxed, or both
|
||||
HEADING_TOLERANCE="both"
|
||||
|
||||
# Step 1: Evaluate ROI0 detections
|
||||
echo ""
|
||||
echo "Step 1: Evaluating Model (${MODEL_NAME}) — ROI0 detections..."
|
||||
echo " Heading tolerance: ${HEADING_TOLERANCE}"
|
||||
echo "--------------------------------------------------------------------------------"
|
||||
MODEL_OUTPUT_ROI0="${OUTPUT_BASE}/${MODEL_NAME}/${TIMESTAMP}_roi0"
|
||||
python eval_tools/core/eval.py \
|
||||
--config ${MODEL_CONFIG_ROI0} \
|
||||
--output-dir ${MODEL_OUTPUT_ROI0} \
|
||||
--heading-tolerance ${HEADING_TOLERANCE} \
|
||||
--save-detailed-matches
|
||||
|
||||
REPORT_JSON_ROI0="${MODEL_OUTPUT_ROI0}/evaluation_report.json"
|
||||
REPORT_MD_ROI0="${MODEL_OUTPUT_ROI0}/EVALUATION_REPORT.md"
|
||||
REPORT_DATE=$(date +%F)
|
||||
|
||||
echo ""
|
||||
echo "Step 2: Generating Markdown report for ROI0..."
|
||||
echo "--------------------------------------------------------------------------------"
|
||||
python eval_tools/model_comparison/generate_eval_report.py \
|
||||
"${REPORT_JSON_ROI0}" \
|
||||
--output "${REPORT_MD_ROI0}" \
|
||||
--model "${MODEL_NAME}-roi0" \
|
||||
--date "${REPORT_DATE}"
|
||||
|
||||
# Step 3: Evaluate ROI1 detections
|
||||
echo ""
|
||||
echo "Step 3: Evaluating Model (${MODEL_NAME}) — ROI1 detections..."
|
||||
echo " Heading tolerance: ${HEADING_TOLERANCE}"
|
||||
echo "--------------------------------------------------------------------------------"
|
||||
MODEL_OUTPUT_ROI1="${OUTPUT_BASE}/${MODEL_NAME}/${TIMESTAMP}_roi1"
|
||||
python eval_tools/core/eval.py \
|
||||
--config ${MODEL_CONFIG_ROI1} \
|
||||
--output-dir ${MODEL_OUTPUT_ROI1} \
|
||||
--heading-tolerance ${HEADING_TOLERANCE} \
|
||||
--save-detailed-matches
|
||||
|
||||
REPORT_JSON_ROI1="${MODEL_OUTPUT_ROI1}/evaluation_report.json"
|
||||
REPORT_MD_ROI1="${MODEL_OUTPUT_ROI1}/EVALUATION_REPORT.md"
|
||||
|
||||
echo ""
|
||||
echo "Step 4: Generating Markdown report for ROI1..."
|
||||
echo "--------------------------------------------------------------------------------"
|
||||
python eval_tools/model_comparison/generate_eval_report.py \
|
||||
"${REPORT_JSON_ROI1}" \
|
||||
--output "${REPORT_MD_ROI1}" \
|
||||
--model "${MODEL_NAME}-roi1" \
|
||||
--date "${REPORT_DATE}"
|
||||
|
||||
echo ""
|
||||
echo "================================================================================"
|
||||
echo "Evaluation completed"
|
||||
echo "--------------------------------------------------------------------------------"
|
||||
echo "ROI0 output: ${MODEL_OUTPUT_ROI0}"
|
||||
echo "ROI0 JSON report: ${REPORT_JSON_ROI0}"
|
||||
echo "ROI0 MD report: ${REPORT_MD_ROI0}"
|
||||
echo "ROI1 output: ${MODEL_OUTPUT_ROI1}"
|
||||
echo "ROI1 JSON report: ${REPORT_JSON_ROI1}"
|
||||
echo "ROI1 MD report: ${REPORT_MD_ROI1}"
|
||||
echo "================================================================================"
|
||||
148
eval_tools/core/run_evaluation_example.sh
Executable file
148
eval_tools/core/run_evaluation_example.sh
Executable file
@@ -0,0 +1,148 @@
|
||||
#!/bin/bash
|
||||
# Example script for running YOLOv5-3D evaluation
|
||||
#
|
||||
# Usage: ./eval_tools/run_evaluation_example.sh
|
||||
|
||||
# =============================================================================
|
||||
# Configuration
|
||||
# =============================================================================
|
||||
|
||||
# Paths (modify these according to your data location)
|
||||
|
||||
|
||||
# Set PYTHONPATH to project root for module imports
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
export PYTHONPATH="${PROJECT_ROOT}:${PYTHONPATH}"
|
||||
GT_PATH="/data1/xdzhu/Testdata_0129"
|
||||
|
||||
OUTPUT_DIR="eval_results/mono3d/$(date +%Y%m%d_%H%M%S)"
|
||||
|
||||
# Image properties
|
||||
IMG_WIDTH=1920
|
||||
IMG_HEIGHT=1080
|
||||
|
||||
# Evaluation parameters
|
||||
IOU_THRESHOLD=0.5
|
||||
CONF_THRESHOLD=0.5
|
||||
AP_METHOD="voc2010" # or "coco"
|
||||
|
||||
DET_PATH="/data1/dongying/Mono3d/G1M3/inference_results/mono3d/evalset_roi0"
|
||||
|
||||
# Distance ranges for 3D evaluation (optional)
|
||||
# Comment out to disable distance-based statistics
|
||||
ENABLE_DISTANCE_RANGES=true
|
||||
DISTANCE_RANGES="[[0, 30], [30, 60], [60, 100], [100, 999]]"
|
||||
|
||||
# =============================================================================
|
||||
# Prepare Configuration
|
||||
# =============================================================================
|
||||
|
||||
# Create temporary config file
|
||||
CONFIG_FILE="/tmp/eval_config_$$.yaml"
|
||||
|
||||
if [ "$ENABLE_DISTANCE_RANGES" = true ]; then
|
||||
cat > "$CONFIG_FILE" << EOF
|
||||
dataset:
|
||||
det_path: "$DET_PATH"
|
||||
gt_path: "$GT_PATH"
|
||||
|
||||
image:
|
||||
width: $IMG_WIDTH
|
||||
height: $IMG_HEIGHT
|
||||
|
||||
matching:
|
||||
iou_threshold: $IOU_THRESHOLD
|
||||
|
||||
metrics_2d:
|
||||
enabled: true
|
||||
conf_threshold: $CONF_THRESHOLD
|
||||
ap_method: "$AP_METHOD"
|
||||
|
||||
metrics_3d:
|
||||
enabled: true
|
||||
distance_ranges:
|
||||
- [0, 30]
|
||||
- [30, 60]
|
||||
- [60, 100]
|
||||
- [100, 999]
|
||||
|
||||
output:
|
||||
save_path: "$OUTPUT_DIR"
|
||||
EOF
|
||||
else
|
||||
cat > "$CONFIG_FILE" << EOF
|
||||
dataset:
|
||||
det_path: "$DET_PATH"
|
||||
gt_path: "$GT_PATH"
|
||||
|
||||
image:
|
||||
width: $IMG_WIDTH
|
||||
height: $IMG_HEIGHT
|
||||
|
||||
matching:
|
||||
iou_threshold: $IOU_THRESHOLD
|
||||
|
||||
metrics_2d:
|
||||
enabled: true
|
||||
conf_threshold: $CONF_THRESHOLD
|
||||
ap_method: "$AP_METHOD"
|
||||
|
||||
metrics_3d:
|
||||
enabled: true
|
||||
|
||||
output:
|
||||
save_path: "$OUTPUT_DIR"
|
||||
EOF
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Run Evaluation
|
||||
# =============================================================================
|
||||
|
||||
echo "=========================================="
|
||||
echo "YOLOv5-3D Model Evaluation"
|
||||
echo "=========================================="
|
||||
echo "Detection path: $DET_PATH"
|
||||
echo "Ground truth path: $GT_PATH"
|
||||
echo "Output directory: $OUTPUT_DIR"
|
||||
if [ "$ENABLE_DISTANCE_RANGES" = true ]; then
|
||||
echo "Distance ranges: ENABLED (0-30m, 30-60m, 60-100m, 100-999m)"
|
||||
else
|
||||
echo "Distance ranges: DISABLED"
|
||||
fi
|
||||
echo "=========================================="
|
||||
|
||||
# Check if paths exist
|
||||
if [ ! -d "$DET_PATH" ]; then
|
||||
echo "Error: Detection path does not exist: $DET_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$GT_PATH" ]; then
|
||||
echo "Error: Ground truth path does not exist: $GT_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run evaluation
|
||||
python eval_tools/core/eval.py \
|
||||
--config "$CONFIG_FILE"
|
||||
|
||||
# Check if evaluation was successful
|
||||
if [ $? -eq 0 ]; then
|
||||
echo ""
|
||||
echo "✓ Evaluation completed successfully!"
|
||||
echo "Results saved to: $OUTPUT_DIR"
|
||||
echo ""
|
||||
echo "View results:"
|
||||
echo " JSON: $OUTPUT_DIR/evaluation_report.json"
|
||||
echo " Text: $OUTPUT_DIR/evaluation_report.txt"
|
||||
|
||||
# Cleanup
|
||||
rm -f "$CONFIG_FILE"
|
||||
else
|
||||
echo ""
|
||||
echo "✗ Evaluation failed!"
|
||||
rm -f "$CONFIG_FILE"
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user