单目3D初始代码

This commit is contained in:
zhao.zhu
2026-06-24 09:35:46 +08:00
commit 04a5895b6b
1153 changed files with 340700 additions and 0 deletions

View 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

View 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()

View 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()

View 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 "========================================="

View 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 "========================================="

File diff suppressed because it is too large Load Diff

View 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