单目3D初始代码
This commit is contained in:
19
eval_tools/heading_analysis/analyze_heading.sh
Executable file
19
eval_tools/heading_analysis/analyze_heading.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Analyze heading errors between two models
|
||||
#
|
||||
# Usage: bash eval_tools/heading_analysis/analyze_heading.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}"
|
||||
|
||||
python eval_tools/heading_analysis/analyze_heading_errors.py \
|
||||
--common-matches eval_results_common_match_comparison_smallset/common_matches_20260203_223240/common_matches.json \
|
||||
--model1-matches eval_results_common_match_comparison_smallset/mono3d/20260203_223240/detailed_3d_matches.json \
|
||||
--model2-matches eval_results_common_match_comparison_smallset/yolov5s-300w/20260203_223240/detailed_3d_matches.json \
|
||||
--model1-name "mono3d" \
|
||||
--model2-name "yolov5s-300w" \
|
||||
--output-dir heading_analysis_results_small \
|
||||
--bad-case-threshold 1.0
|
||||
423
eval_tools/heading_analysis/analyze_heading_errors.py
Executable file
423
eval_tools/heading_analysis/analyze_heading_errors.py
Executable file
@@ -0,0 +1,423 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Heading Error Analysis Tool
|
||||
|
||||
This tool performs comprehensive analysis of heading errors between two models,
|
||||
focusing on understanding why one model has larger heading errors.
|
||||
|
||||
Usage:
|
||||
python eval_tools/analyze_heading_errors.py \\
|
||||
--common-matches eval_results/common_matches.json \\
|
||||
--model1-matches eval_results/model1/detailed_3d_matches.json \\
|
||||
--model2-matches eval_results/model2/detailed_3d_matches.json \\
|
||||
--model1-name "mono3d" \\
|
||||
--model2-name "yolov5s-300w" \\
|
||||
--output-dir heading_analysis_results
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
from pathlib import Path
|
||||
from collections import defaultdict
|
||||
import pandas as pd
|
||||
from scipy import stats
|
||||
|
||||
|
||||
class HeadingErrorAnalyzer:
|
||||
"""Analyze heading errors between two models."""
|
||||
|
||||
def __init__(self, common_matches_data, model1_matches, model2_matches,
|
||||
model1_name="Model1", model2_name="Model2"):
|
||||
"""
|
||||
Initialize analyzer.
|
||||
|
||||
Args:
|
||||
common_matches_data: dict, common matches data
|
||||
model1_matches: dict, model1 detailed matches
|
||||
model2_matches: dict, model2 detailed matches
|
||||
model1_name: str, model1 name
|
||||
model2_name: str, model2 name
|
||||
"""
|
||||
self.common_matches_data = common_matches_data
|
||||
self.model1_matches = model1_matches
|
||||
self.model2_matches = model2_matches
|
||||
self.model1_name = model1_name
|
||||
self.model2_name = model2_name
|
||||
|
||||
# Extract heading errors for common matches
|
||||
self.data = self._extract_heading_data()
|
||||
|
||||
def _extract_heading_data(self):
|
||||
"""Extract heading error data for all common matches."""
|
||||
data = {
|
||||
'model1': defaultdict(list),
|
||||
'model2': defaultdict(list),
|
||||
'common': defaultdict(list)
|
||||
}
|
||||
|
||||
common_matches = self.common_matches_data['common_matches']
|
||||
|
||||
for case_name, frames in common_matches.items():
|
||||
for frame_name, classes in frames.items():
|
||||
for class_name, match_list in classes.items():
|
||||
for match_info in match_list:
|
||||
# Get match indices
|
||||
m1_idx = match_info['model1_idx']
|
||||
m2_idx = match_info['model2_idx']
|
||||
|
||||
# Get match data
|
||||
m1_match = self.model1_matches[case_name][frame_name][class_name][m1_idx]
|
||||
m2_match = self.model2_matches[case_name][frame_name][class_name][m2_idx]
|
||||
|
||||
# Extract information
|
||||
item = {
|
||||
'case': case_name,
|
||||
'frame': frame_name,
|
||||
'class': class_name,
|
||||
'gt_rotation': m1_match.get('gt_rotation', 0),
|
||||
'model1_rotation': m1_match.get('det_rotation', 0),
|
||||
'model2_rotation': m2_match.get('det_rotation', 0),
|
||||
'model1_error': m1_match['errors']['heading'],
|
||||
'model2_error': m2_match['errors']['heading'],
|
||||
'lateral_dist': m1_match['distance']['lateral'],
|
||||
'longitudinal_dist': m1_match['distance']['longitudinal'],
|
||||
'iou': m1_match['iou'],
|
||||
'confidence': m1_match['confidence']
|
||||
}
|
||||
|
||||
data['model1'][class_name].append(m1_match['errors']['heading'])
|
||||
data['model2'][class_name].append(m2_match['errors']['heading'])
|
||||
data['common'][class_name].append(item)
|
||||
|
||||
return data
|
||||
|
||||
def generate_distribution_analysis(self, output_dir):
|
||||
"""Generate distribution analysis and plots."""
|
||||
print("\n" + "="*80)
|
||||
print("Heading Error Distribution Analysis")
|
||||
print("="*80)
|
||||
|
||||
output_dir = Path(output_dir) / 'distribution'
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
results = {}
|
||||
|
||||
for class_name in self.data['model1'].keys():
|
||||
m1_errors = np.array(self.data['model1'][class_name])
|
||||
m2_errors = np.array(self.data['model2'][class_name])
|
||||
|
||||
# Calculate statistics
|
||||
stats_data = {
|
||||
'class': class_name,
|
||||
'count': len(m1_errors),
|
||||
'model1': {
|
||||
'mean': float(np.mean(m1_errors)),
|
||||
'median': float(np.median(m1_errors)),
|
||||
'std': float(np.std(m1_errors)),
|
||||
'p50': float(np.percentile(m1_errors, 50)),
|
||||
'p75': float(np.percentile(m1_errors, 75)),
|
||||
'p90': float(np.percentile(m1_errors, 90)),
|
||||
'p95': float(np.percentile(m1_errors, 95)),
|
||||
'p99': float(np.percentile(m1_errors, 99))
|
||||
},
|
||||
'model2': {
|
||||
'mean': float(np.mean(m2_errors)),
|
||||
'median': float(np.median(m2_errors)),
|
||||
'std': float(np.std(m2_errors)),
|
||||
'p50': float(np.percentile(m2_errors, 50)),
|
||||
'p75': float(np.percentile(m2_errors, 75)),
|
||||
'p90': float(np.percentile(m2_errors, 90)),
|
||||
'p95': float(np.percentile(m2_errors, 95)),
|
||||
'p99': float(np.percentile(m2_errors, 99))
|
||||
}
|
||||
}
|
||||
|
||||
results[class_name] = stats_data
|
||||
|
||||
# Print results
|
||||
print(f"\n{class_name.upper()} (n={stats_data['count']:,}):")
|
||||
print(f" {self.model1_name}:")
|
||||
print(f" Mean: {stats_data['model1']['mean']:.4f} rad")
|
||||
print(f" Median: {stats_data['model1']['median']:.4f} rad")
|
||||
print(f" P90: {stats_data['model1']['p90']:.4f} rad")
|
||||
print(f" P95: {stats_data['model1']['p95']:.4f} rad")
|
||||
print(f" {self.model2_name}:")
|
||||
print(f" Mean: {stats_data['model2']['mean']:.4f} rad")
|
||||
print(f" Median: {stats_data['model2']['median']:.4f} rad")
|
||||
print(f" P90: {stats_data['model2']['p90']:.4f} rad")
|
||||
print(f" P95: {stats_data['model2']['p95']:.4f} rad")
|
||||
print(f" Change:")
|
||||
print(f" Mean: +{(stats_data['model2']['mean'] - stats_data['model1']['mean']):.4f} rad "
|
||||
f"({((stats_data['model2']['mean'] / stats_data['model1']['mean'] - 1) * 100):.1f}%)")
|
||||
|
||||
# Create plots
|
||||
fig, axes = plt.subplots(2, 2, figsize=(15, 12))
|
||||
fig.suptitle(f'Heading Error Distribution - {class_name}', fontsize=16)
|
||||
|
||||
# Histogram
|
||||
axes[0, 0].hist(m1_errors, bins=50, alpha=0.5, label=self.model1_name, density=True)
|
||||
axes[0, 0].hist(m2_errors, bins=50, alpha=0.5, label=self.model2_name, density=True)
|
||||
axes[0, 0].set_xlabel('Heading Error (rad)')
|
||||
axes[0, 0].set_ylabel('Density')
|
||||
axes[0, 0].set_title('Histogram')
|
||||
axes[0, 0].legend()
|
||||
axes[0, 0].grid(True, alpha=0.3)
|
||||
|
||||
# CDF
|
||||
m1_sorted = np.sort(m1_errors)
|
||||
m2_sorted = np.sort(m2_errors)
|
||||
axes[0, 1].plot(m1_sorted, np.arange(len(m1_sorted)) / len(m1_sorted), label=self.model1_name)
|
||||
axes[0, 1].plot(m2_sorted, np.arange(len(m2_sorted)) / len(m2_sorted), label=self.model2_name)
|
||||
axes[0, 1].set_xlabel('Heading Error (rad)')
|
||||
axes[0, 1].set_ylabel('Cumulative Probability')
|
||||
axes[0, 1].set_title('Cumulative Distribution Function')
|
||||
axes[0, 1].legend()
|
||||
axes[0, 1].grid(True, alpha=0.3)
|
||||
|
||||
# Box plot
|
||||
axes[1, 0].boxplot([m1_errors, m2_errors], labels=[self.model1_name, self.model2_name])
|
||||
axes[1, 0].set_ylabel('Heading Error (rad)')
|
||||
axes[1, 0].set_title('Box Plot')
|
||||
axes[1, 0].grid(True, alpha=0.3)
|
||||
|
||||
# Q-Q plot
|
||||
stats.probplot(m2_errors - m1_errors, dist="norm", plot=axes[1, 1])
|
||||
axes[1, 1].set_title('Q-Q Plot (Error Difference)')
|
||||
axes[1, 1].grid(True, alpha=0.3)
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig(output_dir / f'{class_name}_distribution.png', dpi=150)
|
||||
plt.close()
|
||||
|
||||
# Save results
|
||||
with open(output_dir / 'statistics.json', 'w') as f:
|
||||
json.dump(results, f, indent=2)
|
||||
|
||||
print(f"\n✓ Distribution analysis saved to: {output_dir}")
|
||||
return results
|
||||
|
||||
def generate_distance_analysis(self, output_dir):
|
||||
"""Analyze heading errors by distance ranges."""
|
||||
print("\n" + "="*80)
|
||||
print("Heading Error by Distance Analysis")
|
||||
print("="*80)
|
||||
|
||||
output_dir = Path(output_dir) / 'distance'
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Define distance ranges
|
||||
long_ranges = [(0, 20), (20, 40), (40, 60), (60, 80), (80, 100)]
|
||||
lat_ranges = [(-30, -10), (-10, 0), (0, 10), (10, 30)]
|
||||
|
||||
for class_name, items in self.data['common'].items():
|
||||
# Analyze by longitudinal distance
|
||||
long_stats = {}
|
||||
for range_start, range_end in long_ranges:
|
||||
range_key = f"{range_start}-{range_end}m"
|
||||
m1_errors = []
|
||||
m2_errors = []
|
||||
|
||||
for item in items:
|
||||
dist = item['longitudinal_dist']
|
||||
if range_start <= dist < range_end:
|
||||
m1_errors.append(item['model1_error'])
|
||||
m2_errors.append(item['model2_error'])
|
||||
|
||||
if len(m1_errors) > 0:
|
||||
long_stats[range_key] = {
|
||||
'count': len(m1_errors),
|
||||
'model1_mean': float(np.mean(m1_errors)),
|
||||
'model2_mean': float(np.mean(m2_errors)),
|
||||
'diff': float(np.mean(m2_errors) - np.mean(m1_errors))
|
||||
}
|
||||
|
||||
# Plot longitudinal distance analysis
|
||||
if long_stats:
|
||||
fig, ax = plt.subplots(figsize=(12, 6))
|
||||
ranges = list(long_stats.keys())
|
||||
m1_means = [long_stats[r]['model1_mean'] for r in ranges]
|
||||
m2_means = [long_stats[r]['model2_mean'] for r in ranges]
|
||||
|
||||
x = np.arange(len(ranges))
|
||||
width = 0.35
|
||||
|
||||
ax.bar(x - width/2, m1_means, width, label=self.model1_name)
|
||||
ax.bar(x + width/2, m2_means, width, label=self.model2_name)
|
||||
|
||||
ax.set_xlabel('Longitudinal Distance Range')
|
||||
ax.set_ylabel('Mean Heading Error (rad)')
|
||||
ax.set_title(f'Heading Error by Longitudinal Distance - {class_name}')
|
||||
ax.set_xticks(x)
|
||||
ax.set_xticklabels(ranges)
|
||||
ax.legend()
|
||||
ax.grid(True, alpha=0.3)
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig(output_dir / f'{class_name}_longitudinal.png', dpi=150)
|
||||
plt.close()
|
||||
|
||||
print(f"\n{class_name.upper()} - Longitudinal Distance:")
|
||||
for range_key, data in long_stats.items():
|
||||
print(f" {range_key}: {self.model1_name}={data['model1_mean']:.4f}, "
|
||||
f"{self.model2_name}={data['model2_mean']:.4f}, "
|
||||
f"diff={data['diff']:+.4f} (n={data['count']})")
|
||||
|
||||
print(f"\n✓ Distance analysis saved to: {output_dir}")
|
||||
|
||||
def identify_bad_cases(self, output_dir, threshold=1.0):
|
||||
"""Identify cases with large heading errors."""
|
||||
print("\n" + "="*80)
|
||||
print(f"Identifying Bad Cases (threshold > {threshold} rad)")
|
||||
print("="*80)
|
||||
|
||||
output_dir = Path(output_dir) / 'bad_cases'
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
bad_cases = []
|
||||
|
||||
for class_name, items in self.data['common'].items():
|
||||
for item in items:
|
||||
# Check if either model has large error
|
||||
if item['model2_error'] > threshold:
|
||||
bad_cases.append({
|
||||
'case': item['case'],
|
||||
'frame': item['frame'],
|
||||
'class': class_name,
|
||||
'gt_rotation': item['gt_rotation'],
|
||||
'model1_rotation': item['model1_rotation'],
|
||||
'model2_rotation': item['model2_rotation'],
|
||||
'model1_error': item['model1_error'],
|
||||
'model2_error': item['model2_error'],
|
||||
'error_increase': item['model2_error'] - item['model1_error'],
|
||||
'longitudinal_dist': item['longitudinal_dist'],
|
||||
'lateral_dist': item['lateral_dist'],
|
||||
'iou': item['iou'],
|
||||
'confidence': item['confidence']
|
||||
})
|
||||
|
||||
# Sort by model2 error
|
||||
bad_cases.sort(key=lambda x: x['model2_error'], reverse=True)
|
||||
|
||||
# Save to CSV
|
||||
if bad_cases:
|
||||
df = pd.DataFrame(bad_cases)
|
||||
csv_path = output_dir / 'bad_cases.csv'
|
||||
df.to_csv(csv_path, index=False)
|
||||
|
||||
print(f"\nFound {len(bad_cases)} bad cases:")
|
||||
print(f" Saved to: {csv_path}")
|
||||
print(f"\nTop 10 worst cases:")
|
||||
print(df.head(10).to_string(index=False))
|
||||
else:
|
||||
print(f"\nNo bad cases found with threshold > {threshold} rad")
|
||||
|
||||
print(f"\n✓ Bad cases analysis saved to: {output_dir}")
|
||||
return bad_cases
|
||||
|
||||
def generate_summary_report(self, output_dir):
|
||||
"""Generate summary text report."""
|
||||
report_path = output_dir / 'heading_analysis_summary.txt'
|
||||
|
||||
with open(report_path, 'w') as f:
|
||||
f.write("="*80 + "\n")
|
||||
f.write("HEADING ERROR ANALYSIS SUMMARY\n")
|
||||
f.write("="*80 + "\n\n")
|
||||
|
||||
f.write(f"Model 1: {self.model1_name}\n")
|
||||
f.write(f"Model 2: {self.model2_name}\n\n")
|
||||
|
||||
f.write("Overall Statistics by Class:\n")
|
||||
f.write("-"*80 + "\n\n")
|
||||
|
||||
for class_name in self.data['model1'].keys():
|
||||
m1_errors = np.array(self.data['model1'][class_name])
|
||||
m2_errors = np.array(self.data['model2'][class_name])
|
||||
|
||||
f.write(f"{class_name.upper()} (n={len(m1_errors):,}):\n")
|
||||
f.write(f" {self.model1_name}:\n")
|
||||
f.write(f" Mean: {np.mean(m1_errors):.4f} rad ({np.degrees(np.mean(m1_errors)):.2f}°)\n")
|
||||
f.write(f" Median: {np.median(m1_errors):.4f} rad ({np.degrees(np.median(m1_errors)):.2f}°)\n")
|
||||
f.write(f" Std: {np.std(m1_errors):.4f} rad\n")
|
||||
f.write(f" {self.model2_name}:\n")
|
||||
f.write(f" Mean: {np.mean(m2_errors):.4f} rad ({np.degrees(np.mean(m2_errors)):.2f}°)\n")
|
||||
f.write(f" Median: {np.median(m2_errors):.4f} rad ({np.degrees(np.median(m2_errors)):.2f}°)\n")
|
||||
f.write(f" Std: {np.std(m2_errors):.4f} rad\n")
|
||||
|
||||
diff_mean = np.mean(m2_errors) - np.mean(m1_errors)
|
||||
pct_change = (np.mean(m2_errors) / np.mean(m1_errors) - 1) * 100
|
||||
f.write(f" Change:\n")
|
||||
f.write(f" Mean: +{diff_mean:.4f} rad ({pct_change:+.2f}%)\n\n")
|
||||
|
||||
print(f"\n✓ Summary report saved to: {report_path}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function."""
|
||||
parser = argparse.ArgumentParser(description='Analyze heading errors between two models')
|
||||
parser.add_argument('--common-matches', type=str, required=True,
|
||||
help='Path to common_matches.json')
|
||||
parser.add_argument('--model1-matches', type=str, required=True,
|
||||
help='Path to model1 detailed_3d_matches.json')
|
||||
parser.add_argument('--model2-matches', type=str, required=True,
|
||||
help='Path to model2 detailed_3d_matches.json')
|
||||
parser.add_argument('--model1-name', type=str, default='Model1',
|
||||
help='Name of model 1')
|
||||
parser.add_argument('--model2-name', type=str, default='Model2',
|
||||
help='Name of model 2')
|
||||
parser.add_argument('--output-dir', type=str, default='heading_analysis',
|
||||
help='Output directory for analysis results')
|
||||
parser.add_argument('--bad-case-threshold', type=float, default=1.0,
|
||||
help='Threshold for identifying bad cases (radians)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Load data
|
||||
print("Loading data...")
|
||||
with open(args.common_matches, 'r') as f:
|
||||
common_matches_data = json.load(f)
|
||||
|
||||
with open(args.model1_matches, 'r') as f:
|
||||
model1_matches = json.load(f)
|
||||
|
||||
with open(args.model2_matches, 'r') as f:
|
||||
model2_matches = json.load(f)
|
||||
|
||||
# Create analyzer
|
||||
analyzer = HeadingErrorAnalyzer(
|
||||
common_matches_data,
|
||||
model1_matches,
|
||||
model2_matches,
|
||||
model1_name=args.model1_name,
|
||||
model2_name=args.model2_name
|
||||
)
|
||||
|
||||
# Create output directory
|
||||
output_dir = Path(args.output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Run analyses
|
||||
print("\n" + "="*80)
|
||||
print("HEADING ERROR ANALYSIS")
|
||||
print("="*80)
|
||||
|
||||
analyzer.generate_distribution_analysis(output_dir)
|
||||
analyzer.generate_distance_analysis(output_dir)
|
||||
analyzer.identify_bad_cases(output_dir, threshold=args.bad_case_threshold)
|
||||
analyzer.generate_summary_report(output_dir)
|
||||
|
||||
print("\n" + "="*80)
|
||||
print("ANALYSIS COMPLETE!")
|
||||
print("="*80)
|
||||
print(f"\nResults saved to: {output_dir}/")
|
||||
print("\nGenerated files:")
|
||||
print(" - heading_analysis_summary.txt: Text summary report")
|
||||
print(" - distribution/: Error distribution analysis and plots")
|
||||
print(" - distance/: Distance-based analysis and plots")
|
||||
print(" - bad_cases/bad_cases.csv: List of cases with large errors")
|
||||
print("")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
313
eval_tools/heading_analysis/extract_bad_heading_cases.py
Executable file
313
eval_tools/heading_analysis/extract_bad_heading_cases.py
Executable file
@@ -0,0 +1,313 @@
|
||||
"""
|
||||
Extract bad heading error cases from detailed_3d_matches.json
|
||||
|
||||
This tool extracts and analyzes cases with large heading errors for visualization.
|
||||
|
||||
Usage:
|
||||
python eval_tools/extract_bad_heading_cases.py \
|
||||
--input eval_results_common_match_comparison/yolov5s-300w/20260203_210259/detailed_3d_matches.json \
|
||||
--threshold 1.5 \
|
||||
--top-k 100 \
|
||||
--output bad_heading_cases.json
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from collections import defaultdict
|
||||
import numpy as np
|
||||
|
||||
# Allow importing class_config from the eval_tools root
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
from class_config import CLASSES_3D as _CLASSES_3D, CLASS_NAMES as _CLASS_NAMES
|
||||
_CLASSES_3D_NAMES = set(_CLASS_NAMES[i] for i in _CLASSES_3D)
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description='Extract bad heading error cases')
|
||||
parser.add_argument('--input', type=str, required=True,
|
||||
help='Path to detailed_3d_matches.json')
|
||||
parser.add_argument('--threshold', type=float, default=1.5,
|
||||
help='Heading error threshold in radians (default: 1.5 ≈ 85°)')
|
||||
parser.add_argument('--top-k', type=int, default=None,
|
||||
help='Only extract top K worst cases (default: all)')
|
||||
parser.add_argument('--classes', nargs='+', default=None,
|
||||
help='Filter by classes (e.g., vehicle pedestrian bicycle rider)')
|
||||
parser.add_argument('--min-distance', type=float, default=None,
|
||||
help='Minimum distance in meters')
|
||||
parser.add_argument('--max-distance', type=float, default=None,
|
||||
help='Maximum distance in meters')
|
||||
parser.add_argument('--min-confidence', type=float, default=None,
|
||||
help='Minimum detection confidence')
|
||||
parser.add_argument('--reversal-only', action='store_true',
|
||||
help='Only extract reversal errors (error > π - 0.1)')
|
||||
parser.add_argument('--output', type=str, default='bad_heading_cases.json',
|
||||
help='Output JSON file path')
|
||||
parser.add_argument('--stats', action='store_true',
|
||||
help='Print statistics')
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def calculate_distance_3d(x, y, z):
|
||||
"""Calculate 3D Euclidean distance from ego vehicle."""
|
||||
return np.sqrt(x**2 + y**2 + z**2)
|
||||
|
||||
|
||||
def is_reversal_error(error, threshold=0.1):
|
||||
"""Check if error is a reversal error (≈ π or 180°)."""
|
||||
return error > (np.pi - threshold)
|
||||
|
||||
|
||||
def extract_bad_cases(data, args):
|
||||
"""Extract bad heading error cases based on criteria.
|
||||
|
||||
Args:
|
||||
data: Loaded JSON data from detailed_3d_matches.json
|
||||
args: Command line arguments
|
||||
|
||||
Returns:
|
||||
List of bad cases with metadata
|
||||
"""
|
||||
bad_cases = []
|
||||
stats = defaultdict(lambda: {'count': 0, 'reversal_count': 0, 'total_error': 0})
|
||||
|
||||
total_processed = 0
|
||||
|
||||
# Iterate through all matches
|
||||
for case_id, case_data in data.items():
|
||||
for frame_id, frame_data in case_data.items():
|
||||
if not isinstance(frame_data, dict):
|
||||
continue
|
||||
|
||||
for class_name, class_data in frame_data.items():
|
||||
if class_name not in _CLASSES_3D_NAMES:
|
||||
continue
|
||||
|
||||
# Filter by class if specified
|
||||
if args.classes and class_name not in args.classes:
|
||||
continue
|
||||
|
||||
# class_data is already the list of matches
|
||||
matches = class_data if isinstance(class_data, list) else []
|
||||
|
||||
for match in matches:
|
||||
total_processed += 1
|
||||
|
||||
# Extract key information
|
||||
# heading_error is in errors['heading'], not directly in match
|
||||
errors = match.get('errors', {})
|
||||
heading_error = errors.get('heading', 0) if isinstance(errors, dict) else 0
|
||||
lateral_error = errors.get('lateral', 0) if isinstance(errors, dict) else 0
|
||||
longitudinal_error = errors.get('longitudinal', 0) if isinstance(errors, dict) else 0
|
||||
|
||||
gt_rotation = match.get('gt_rotation', 0)
|
||||
det_rotation = match.get('det_rotation', 0)
|
||||
|
||||
# Get 3D center coordinates
|
||||
gt_center = match.get('gt_center_3d', [0, 0, 0])
|
||||
det_center = match.get('det_center_3d', [0, 0, 0])
|
||||
|
||||
# Calculate distance
|
||||
distance = calculate_distance_3d(*gt_center)
|
||||
|
||||
# Get other metadata
|
||||
confidence = match.get('confidence', 0)
|
||||
iou = match.get('iou', 0)
|
||||
|
||||
# Apply filters
|
||||
if heading_error < args.threshold:
|
||||
continue
|
||||
|
||||
if args.min_distance and distance < args.min_distance:
|
||||
continue
|
||||
|
||||
if args.max_distance and distance > args.max_distance:
|
||||
continue
|
||||
|
||||
if args.min_confidence and confidence < args.min_confidence:
|
||||
continue
|
||||
|
||||
if args.reversal_only and not is_reversal_error(heading_error):
|
||||
continue
|
||||
|
||||
# Update statistics
|
||||
stats[class_name]['count'] += 1
|
||||
stats[class_name]['total_error'] += heading_error
|
||||
if is_reversal_error(heading_error):
|
||||
stats[class_name]['reversal_count'] += 1
|
||||
|
||||
# Create case entry
|
||||
case_entry = {
|
||||
'case_id': case_id,
|
||||
'frame_id': frame_id,
|
||||
'class': class_name,
|
||||
'heading_error': float(heading_error),
|
||||
'heading_error_deg': float(np.degrees(heading_error)),
|
||||
'gt_rotation': float(gt_rotation),
|
||||
'gt_rotation_deg': float(np.degrees(gt_rotation)),
|
||||
'det_rotation': float(det_rotation),
|
||||
'det_rotation_deg': float(np.degrees(det_rotation)),
|
||||
'is_reversal': is_reversal_error(heading_error),
|
||||
'distance': float(distance),
|
||||
'confidence': float(confidence),
|
||||
'iou': float(iou),
|
||||
'gt_center': [float(x) for x in gt_center],
|
||||
'det_center': [float(x) for x in det_center],
|
||||
'lateral_error': float(lateral_error),
|
||||
'longitudinal_error': float(longitudinal_error),
|
||||
'gt_bbox_2d': match.get('gt_bbox', [0, 0, 0, 0]),
|
||||
'det_bbox_2d': match.get('det_bbox', [0, 0, 0, 0]),
|
||||
}
|
||||
|
||||
bad_cases.append(case_entry)
|
||||
|
||||
# Sort by heading error (descending)
|
||||
bad_cases.sort(key=lambda x: x['heading_error'], reverse=True)
|
||||
|
||||
# Limit to top-k if specified
|
||||
if args.top_k:
|
||||
bad_cases = bad_cases[:args.top_k]
|
||||
|
||||
return bad_cases, stats, total_processed
|
||||
|
||||
|
||||
def print_statistics(bad_cases, stats, total_processed, args):
|
||||
"""Print detailed statistics about extracted cases."""
|
||||
print("\n" + "="*80)
|
||||
print("BAD HEADING ERROR CASES EXTRACTION SUMMARY")
|
||||
print("="*80)
|
||||
|
||||
print(f"\nInput file: {args.input}")
|
||||
print(f"Threshold: {args.threshold:.2f} rad ({np.degrees(args.threshold):.1f}°)")
|
||||
print(f"Total processed: {total_processed:,}")
|
||||
print(f"Bad cases found: {len(bad_cases):,} ({100*len(bad_cases)/total_processed:.2f}%)")
|
||||
|
||||
if args.top_k:
|
||||
print(f"Output limited to: Top {args.top_k}")
|
||||
|
||||
print("\n" + "-"*80)
|
||||
print("STATISTICS BY CLASS")
|
||||
print("-"*80)
|
||||
print(f"{'Class':<15} {'Count':<10} {'Reversal':<12} {'Rev %':<10} {'Avg Error':<12}")
|
||||
print("-"*80)
|
||||
|
||||
for class_name in sorted(stats.keys()):
|
||||
stat = stats[class_name]
|
||||
count = stat['count']
|
||||
rev_count = stat['reversal_count']
|
||||
avg_error = stat['total_error'] / count if count > 0 else 0
|
||||
rev_pct = 100 * rev_count / count if count > 0 else 0
|
||||
|
||||
print(f"{class_name:<15} {count:<10,} {rev_count:<12,} {rev_pct:<10.1f} {avg_error:<12.3f}")
|
||||
|
||||
print("-"*80)
|
||||
|
||||
# Error distribution
|
||||
if bad_cases:
|
||||
errors = [c['heading_error'] for c in bad_cases]
|
||||
print(f"\nERROR DISTRIBUTION:")
|
||||
print(f" Min: {min(errors):.3f} rad ({np.degrees(min(errors)):.1f}°)")
|
||||
print(f" Max: {max(errors):.3f} rad ({np.degrees(max(errors)):.1f}°)")
|
||||
print(f" Mean: {np.mean(errors):.3f} rad ({np.degrees(np.mean(errors)):.1f}°)")
|
||||
print(f" Median: {np.median(errors):.3f} rad ({np.degrees(np.median(errors)):.1f}°)")
|
||||
print(f" Std: {np.std(errors):.3f} rad ({np.degrees(np.std(errors)):.1f}°)")
|
||||
|
||||
# Reversal statistics
|
||||
reversal_count = sum(1 for c in bad_cases if c['is_reversal'])
|
||||
print(f"\n Reversal errors (>3.04 rad): {reversal_count} ({100*reversal_count/len(bad_cases):.1f}%)")
|
||||
|
||||
# Distance distribution
|
||||
distances = [c['distance'] for c in bad_cases]
|
||||
print(f"\nDISTANCE DISTRIBUTION:")
|
||||
print(f" Min: {min(distances):.1f} m")
|
||||
print(f" Max: {max(distances):.1f} m")
|
||||
print(f" Mean: {np.mean(distances):.1f} m")
|
||||
print(f" Median: {np.median(distances):.1f} m")
|
||||
|
||||
# Confidence distribution
|
||||
confidences = [c['confidence'] for c in bad_cases]
|
||||
print(f"\nCONFIDENCE DISTRIBUTION:")
|
||||
print(f" Min: {min(confidences):.3f}")
|
||||
print(f" Max: {max(confidences):.3f}")
|
||||
print(f" Mean: {np.mean(confidences):.3f}")
|
||||
print(f" Median: {np.median(confidences):.3f}")
|
||||
|
||||
print("\n" + "="*80)
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
|
||||
# Load input JSON
|
||||
input_path = Path(args.input)
|
||||
if not input_path.exists():
|
||||
print(f"Error: Input file not found: {input_path}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Loading data from {input_path}...")
|
||||
with open(input_path, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
print(f"Loaded {len(data)} cases")
|
||||
|
||||
# Extract bad cases
|
||||
print(f"\nExtracting bad cases with heading_error > {args.threshold:.2f} rad...")
|
||||
bad_cases, stats, total_processed = extract_bad_cases(data, args)
|
||||
|
||||
# Print statistics
|
||||
if args.stats or len(bad_cases) > 0:
|
||||
print_statistics(bad_cases, stats, total_processed, args)
|
||||
|
||||
# Save output
|
||||
output_path = Path(args.output)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
output_data = {
|
||||
'metadata': {
|
||||
'source': str(input_path),
|
||||
'threshold': args.threshold,
|
||||
'threshold_degrees': float(np.degrees(args.threshold)),
|
||||
'total_processed': total_processed,
|
||||
'total_extracted': len(bad_cases),
|
||||
'filters': {
|
||||
'classes': args.classes,
|
||||
'min_distance': args.min_distance,
|
||||
'max_distance': args.max_distance,
|
||||
'min_confidence': args.min_confidence,
|
||||
'reversal_only': args.reversal_only,
|
||||
'top_k': args.top_k
|
||||
}
|
||||
},
|
||||
'statistics': {
|
||||
class_name: {
|
||||
'count': stat['count'],
|
||||
'reversal_count': stat['reversal_count'],
|
||||
'reversal_percentage': 100 * stat['reversal_count'] / stat['count'] if stat['count'] > 0 else 0,
|
||||
'avg_error': stat['total_error'] / stat['count'] if stat['count'] > 0 else 0
|
||||
}
|
||||
for class_name, stat in stats.items()
|
||||
},
|
||||
'cases': bad_cases
|
||||
}
|
||||
|
||||
with open(output_path, 'w') as f:
|
||||
json.dump(output_data, f, indent=2)
|
||||
|
||||
print(f"\nExtracted {len(bad_cases)} bad cases")
|
||||
print(f"Output saved to: {output_path}")
|
||||
|
||||
# Show top 5 worst cases
|
||||
if bad_cases:
|
||||
print(f"\nTop 5 Worst Cases:")
|
||||
print("-"*80)
|
||||
print(f"{'No':<5} {'Class':<12} {'Error (rad)':<12} {'Error (°)':<12} {'Distance':<12} {'Reversal':<10}")
|
||||
print("-"*80)
|
||||
for i, case in enumerate(bad_cases[:5], 1):
|
||||
print(f"{i:<5} {case['class']:<12} {case['heading_error']:<12.3f} "
|
||||
f"{case['heading_error_deg']:<12.1f} {case['distance']:<12.1f} "
|
||||
f"{'✓' if case['is_reversal'] else '':<10}")
|
||||
print("-"*80)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
88
eval_tools/heading_analysis/test_extract_bad_cases.sh
Executable file
88
eval_tools/heading_analysis/test_extract_bad_cases.sh
Executable file
@@ -0,0 +1,88 @@
|
||||
#!/bin/bash
|
||||
# Test script for extracting bad heading cases
|
||||
|
||||
|
||||
# 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 "Bad Heading Cases Extraction Test"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
|
||||
INPUT_FILE="eval_results_common_match_comparison/yolov5s-300w/20260203_210259/detailed_3d_matches.json"
|
||||
OUTPUT_DIR="runs/heading_error_analysis"
|
||||
|
||||
# Check if input file exists
|
||||
if [ ! -f "$INPUT_FILE" ]; then
|
||||
echo "Error: Input file not found: $INPUT_FILE"
|
||||
echo "Please update INPUT_FILE path in this script"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
echo "Test 1: Extract all cases with error > 1.5 rad (≈ 85°)"
|
||||
echo "-----------------------------------------------------"
|
||||
python eval_tools/heading_analysis/extract_bad_heading_cases.py \
|
||||
--input "$INPUT_FILE" \
|
||||
--threshold 1.5 \
|
||||
--output "$OUTPUT_DIR/all_bad_cases.json" \
|
||||
--stats
|
||||
|
||||
echo ""
|
||||
echo ""
|
||||
echo "Test 2: Extract top 50 worst cases"
|
||||
echo "-----------------------------------------------------"
|
||||
python eval_tools/heading_analysis/extract_bad_heading_cases.py \
|
||||
--input "$INPUT_FILE" \
|
||||
--threshold 1.5 \
|
||||
--top-k 50 \
|
||||
--output "$OUTPUT_DIR/top50_worst.json" \
|
||||
--stats
|
||||
|
||||
echo ""
|
||||
echo ""
|
||||
echo "Test 3: Extract only reversal errors (≈ 180°)"
|
||||
echo "-----------------------------------------------------"
|
||||
python eval_tools/heading_analysis/extract_bad_heading_cases.py \
|
||||
--input "$INPUT_FILE" \
|
||||
--threshold 1.5 \
|
||||
--reversal-only \
|
||||
--top-k 100 \
|
||||
--output "$OUTPUT_DIR/reversal_errors.json" \
|
||||
--stats
|
||||
|
||||
echo ""
|
||||
echo ""
|
||||
echo "Test 4: Extract vehicle class only, high confidence"
|
||||
echo "-----------------------------------------------------"
|
||||
python eval_tools/heading_analysis/extract_bad_heading_cases.py \
|
||||
--input "$INPUT_FILE" \
|
||||
--threshold 1.5 \
|
||||
--classes vehicle \
|
||||
--min-confidence 0.5 \
|
||||
--top-k 50 \
|
||||
--output "$OUTPUT_DIR/vehicle_high_conf.json" \
|
||||
--stats
|
||||
|
||||
echo ""
|
||||
echo ""
|
||||
echo "Test 5: Extract mid-distance cases (30-80m)"
|
||||
echo "-----------------------------------------------------"
|
||||
python eval_tools/heading_analysis/extract_bad_heading_cases.py \
|
||||
--input "$INPUT_FILE" \
|
||||
--threshold 1.5 \
|
||||
--min-distance 30 \
|
||||
--max-distance 80 \
|
||||
--top-k 50 \
|
||||
--output "$OUTPUT_DIR/mid_distance.json" \
|
||||
--stats
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo "All tests complete!"
|
||||
echo "Output directory: $OUTPUT_DIR"
|
||||
echo "========================================="
|
||||
62
eval_tools/heading_analysis/test_visualize_heading.sh
Executable file
62
eval_tools/heading_analysis/test_visualize_heading.sh
Executable file
@@ -0,0 +1,62 @@
|
||||
#!/bin/bash
|
||||
# Test script for visualizing heading errors
|
||||
|
||||
|
||||
# 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 "Heading Error Visualization Test"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
|
||||
INPUT_FILE="bad_heading_cases_top50.json"
|
||||
IMAGE_ROOT="/path/to/your/image/root" # UPDATE THIS
|
||||
OUTPUT_DIR="runs/heading_viz_test"
|
||||
|
||||
# Check if input file exists
|
||||
if [ ! -f "$INPUT_FILE" ]; then
|
||||
echo "Error: Input file not found: $INPUT_FILE"
|
||||
echo "Please run extract_bad_heading_cases.py first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Test 1: Generate all visualization types (first 10 cases)"
|
||||
echo "-----------------------------------------------------"
|
||||
python eval_tools/heading_analysis/visualize_heading_errors.py \
|
||||
--input "$INPUT_FILE" \
|
||||
--image-root "$IMAGE_ROOT" \
|
||||
--output "$OUTPUT_DIR/all_viz" \
|
||||
--max-cases 10 \
|
||||
--viz-types bev image angle combined
|
||||
|
||||
echo ""
|
||||
echo ""
|
||||
echo "Test 2: Generate only BEV views (first 20 cases)"
|
||||
echo "-----------------------------------------------------"
|
||||
python eval_tools/heading_analysis/visualize_heading_errors.py \
|
||||
--input "$INPUT_FILE" \
|
||||
--image-root "$IMAGE_ROOT" \
|
||||
--output "$OUTPUT_DIR/bev_only" \
|
||||
--max-cases 20 \
|
||||
--viz-types bev
|
||||
|
||||
echo ""
|
||||
echo ""
|
||||
echo "Test 3: Generate combined views (first 5 cases, high DPI)"
|
||||
echo "-----------------------------------------------------"
|
||||
python eval_tools/heading_analysis/visualize_heading_errors.py \
|
||||
--input "$INPUT_FILE" \
|
||||
--image-root "$IMAGE_ROOT" \
|
||||
--output "$OUTPUT_DIR/combined_hq" \
|
||||
--max-cases 5 \
|
||||
--viz-types combined \
|
||||
--dpi 150
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo "All tests complete!"
|
||||
echo "Output directory: $OUTPUT_DIR"
|
||||
echo "========================================="
|
||||
1162
eval_tools/heading_analysis/visualize_heading_errors.py
Executable file
1162
eval_tools/heading_analysis/visualize_heading_errors.py
Executable file
File diff suppressed because it is too large
Load Diff
13
eval_tools/heading_analysis/visualize_heading_errors.sh
Executable file
13
eval_tools/heading_analysis/visualize_heading_errors.sh
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Set PYTHONPATH to project root for module imports
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
export PYTHONPATH="${PROJECT_ROOT}:${PYTHONPATH}"
|
||||
python eval_tools/heading_analysis/visualize_heading_errors.py \
|
||||
--input bad_heading_cases.json \
|
||||
--image-root /data1/xdzhu/Testdata_0129 \
|
||||
--output runs/heading_viz \
|
||||
--viz-types combined \
|
||||
--merge-same-frame \
|
||||
--workers 8
|
||||
Reference in New Issue
Block a user