# 基于共同匹配集的3D指标对比方案

## 问题分析

### 当前问题
两个模型评测时匹配的样本数量不一致：
- **Model A**: 匹配 440,408 个 vehicle
- **Model B**: 匹配 491,768 个 vehicle
- 差异：51,360 个样本（11.7%）

**导致的问题**：
- 无法确定性能差异是因为**模型质量**还是**匹配了不同的目标**
- 比较的不是相同目标的预测质量

### 解决方案
只比较两个模型**都成功匹配到GT的目标**，确保对比的是相同目标的3D性能差异。

## 实现方案

### 方案架构

```
┌─────────────────┐
│  Model 1 Eval   │
│  + Save Matches │──┐
└─────────────────┘  │
                     │  ┌──────────────────────┐
                     ├─→│ Common Match Finder  │
                     │  └──────────────────────┘
┌─────────────────┐  │            ↓
│  Model 2 Eval   │  │  ┌──────────────────────┐
│  + Save Matches │──┘  │ Recompute 3D Stats   │
└─────────────────┘     │ (Common Matches Only)│
                        └──────────────────────┘
                                   ↓
                        ┌──────────────────────┐
                        │  Comparison Report   │
                        └──────────────────────┘
```

### 数据结构设计

#### 1. 详细匹配数据文件 (`detailed_3d_matches.json`)

```json
{
  "case_019b178d": {
    "frame_000018": {
      "vehicle": [
        {
          "gt_id": "hash_of_gt_bbox",  // 用于唯一标识GT
          "gt_bbox": [90.48, 561.98, 241.52, 714.00],
          "gt_center_3d": [-7.13, 0.91, 8.20],
          "det_bbox": [91.2, 562.5, 240.8, 713.2],
          "det_center_3d": [-7.25, 0.89, 8.35],
          "iou": 0.89,
          "confidence": 0.87478,
          "errors": {
            "lateral": 0.12,
            "longitudinal": 0.15,
            "heading": 0.05
          },
          "distance": {
            "longitudinal": 8.20,
            "lateral": -7.13
          }
        }
      ]
    }
  }
}
```

#### 2. 共同匹配索引 (`common_matches_index.json`)

```json
{
  "statistics": {
    "model1_total": 440408,
    "model2_total": 491768,
    "common_matches": 425163,
    "model1_unique": 15245,
    "model2_unique": 66605,
    "common_percentage": 96.5
  },
  "common_matches": {
    "case_019b178d": {
      "frame_000018": {
        "vehicle": [
          {
            "gt_id": "hash_of_gt_bbox",
            "model1_idx": 0,
            "model2_idx": 0
          }
        ]
      }
    }
  }
}
```

### 实现步骤

#### 步骤1：扩展评测器保存详细匹配

修改 `eval_tools/evaluator/evaluator.py`：

```python
def evaluate_3d(self):
    # ... 现有代码 ...
    
    # 新增：保存详细匹配信息
    self.detailed_3d_matches = {}
    
    for case_name, case_pairs in cases.items():
        self.detailed_3d_matches[case_name] = {}
        
        for pair in case_pairs:
            frame_name = pair['frame']
            gts = self.gt_parser.parse_file(...)
            dets = self.det_parser.parse_file(...)
            
            self.detailed_3d_matches[case_name][frame_name] = {}
            
            for class_id in Metrics3D.CLASSES_3D:
                match_result = self.matcher.match(gts, dets, class_id)
                matches_list = []
                
                for gt_idx, det_idx, iou in match_result['matches']:
                    gt = match_result['gts_filtered'][gt_idx]
                    det = match_result['dets_sorted'][det_idx]
                    
                    if gt['has_3d'] and det['3d_info']:
                        # 计算3D误差
                        errors = self._compute_3d_errors(gt, det)
                        
                        # 保存匹配详情
                        matches_list.append({
                            'gt_id': self._generate_gt_id(gt),
                            'gt_bbox': gt['bbox_2d'],
                            'det_bbox': det['bbox_2d'],
                            'errors': errors,
                            'distance': {
                                'longitudinal': gt['3d_info']['center'][2],
                                'lateral': gt['3d_info']['center'][0]
                            }
                        })
                
                if matches_list:
                    class_name = self.gt_parser.get_class_name(class_id)
                    self.detailed_3d_matches[case_name][frame_name][class_name] = matches_list
```

#### 步骤2：创建共同匹配查找工具

新建 `eval_tools/find_common_matches.py`：

```python
def find_common_matches(model1_matches, model2_matches):
    """
    找出两个模型共同匹配的GT目标
    
    Args:
        model1_matches: Model 1的详细匹配数据
        model2_matches: Model 2的详细匹配数据
    
    Returns:
        common_matches: 共同匹配的索引
        stats: 统计信息
    """
    common_matches = {}
    stats = {
        'model1_total': 0,
        'model2_total': 0,
        'common': 0,
        'model1_unique': 0,
        'model2_unique': 0
    }
    
    for case_name in model1_matches:
        if case_name not in model2_matches:
            continue
        
        common_matches[case_name] = {}
        
        for frame_name in model1_matches[case_name]:
            if frame_name not in model2_matches[case_name]:
                continue
            
            common_matches[case_name][frame_name] = {}
            
            for class_name in model1_matches[case_name][frame_name]:
                if class_name not in model2_matches[case_name][frame_name]:
                    continue
                
                m1_list = model1_matches[case_name][frame_name][class_name]
                m2_list = model2_matches[case_name][frame_name][class_name]
                
                # 建立GT ID映射
                m1_gt_ids = {m['gt_id']: i for i, m in enumerate(m1_list)}
                m2_gt_ids = {m['gt_id']: i for i, m in enumerate(m2_list)}
                
                # 找共同的GT ID
                common_gt_ids = set(m1_gt_ids.keys()) & set(m2_gt_ids.keys())
                
                stats['model1_total'] += len(m1_list)
                stats['model2_total'] += len(m2_list)
                stats['common'] += len(common_gt_ids)
                stats['model1_unique'] += len(m1_gt_ids) - len(common_gt_ids)
                stats['model2_unique'] += len(m2_gt_ids) - len(common_gt_ids)
                
                # 保存共同匹配的索引
                common_list = []
                for gt_id in common_gt_ids:
                    common_list.append({
                        'gt_id': gt_id,
                        'model1_idx': m1_gt_ids[gt_id],
                        'model2_idx': m2_gt_ids[gt_id]
                    })
                
                if common_list:
                    common_matches[case_name][frame_name][class_name] = common_list
    
    return common_matches, stats
```

#### 步骤3：扩展模型比较器

修改 `eval_tools/compare_models.py`：

```python
class ModelComparator:
    def __init__(self, model1_report, model2_report, 
                 model1_matches=None, model2_matches=None,
                 use_common_matches=False):
        """
        Args:
            use_common_matches: 是否只比较共同匹配的样本
        """
        self.use_common_matches = use_common_matches
        
        if use_common_matches:
            # 找出共同匹配
            self.common_matches, self.common_stats = find_common_matches(
                model1_matches, model2_matches
            )
            
            # 基于共同匹配重新计算3D统计
            self.model1_3d_filtered = self._recompute_3d_stats(
                model1_matches, 'model1'
            )
            self.model2_3d_filtered = self._recompute_3d_stats(
                model2_matches, 'model2'
            )
    
    def _recompute_3d_stats(self, matches_data, model_name):
        """基于共同匹配重新计算3D统计"""
        stats = {}
        
        for case_name, frames in self.common_matches.items():
            for frame_name, classes in frames.items():
                for class_name, common_list in classes.items():
                    if class_name not in stats:
                        stats[class_name] = {
                            'lateral': [],
                            'longitudinal': [],
                            'heading': []
                        }
                    
                    for match_info in common_list:
                        idx = match_info[f'{model_name}_idx']
                        match = matches_data[case_name][frame_name][class_name][idx]
                        
                        stats[class_name]['lateral'].append(match['errors']['lateral'])
                        stats[class_name]['longitudinal'].append(match['errors']['longitudinal'])
                        stats[class_name]['heading'].append(match['errors']['heading'])
        
        return stats
```

### 使用流程

#### 1. 评测并保存详细匹配

```bash
# 评测Model 1
python eval_tools/eval.py \
  --config eval_tools/configs/eval_config_mono3d.yaml \
  --save-detailed-matches

# 评测Model 2
python eval_tools/eval.py \
  --config eval_tools/configs/eval_config_yolov5.yaml \
  --save-detailed-matches
```

#### 2. 比较（基于共同匹配）

```bash
python eval_tools/compare_models_with_common_matches.py \
  --model1-report eval_results_multiprocess/mono3d/20260203_162537/evaluation_report.json \
  --model1-matches eval_results_multiprocess/mono3d/20260203_162537/detailed_3d_matches.json \
  --model2-report eval_results_multiprocess/yolov5s/20260203_161644/evaluation_report.json \
  --model2-matches eval_results_multiprocess/yolov5s/20260203_161644/detailed_3d_matches.json \
  --output-dir comparison_results_common_matches \
  --use-common-matches
```

#### 3. 对比报告示例

```
================================================================================
3D METRICS COMPARISON (COMMON MATCHES ONLY)
================================================================================

Match Statistics:
  Model 1 Total Matches: 440,408
  Model 2 Total Matches: 491,768
  Common Matches: 425,163 (96.5% of Model 1)
  Model 1 Unique: 15,245 (3.5%)
  Model 2 Unique: 66,605 (13.5%)

VEHICLE (Common Matches: 420,158):
Metric               mono3d          yolov5s-300w    Diff            Change %     
------------------------------------------------------------------------------------
Lateral (m)          1.3251          1.2103          -0.1148         -8.66%       ✓
Longitudinal (m)     2.5892          2.4231          -0.1661         -6.42%       ✓
Heading (rad)        0.2256          0.3098          +0.0842         +37.32%      ✗

Note: 这些统计基于两个模型都成功匹配的目标，排除了只被一个模型匹配的目标。
```

## 优势

1. **公平对比**：确保比较的是相同目标的预测质量
2. **性能分析**：可以单独分析各模型独有匹配的特点
3. **问题诊断**：识别哪些目标一个模型能匹配而另一个不能

## 可选扩展

### 1. 分析独有匹配

```python
def analyze_unique_matches(model1_matches, model2_matches, common_matches):
    """分析每个模型独有匹配的特征"""
    model1_unique = []
    model2_unique = []
    
    # ... 找出独有匹配 ...
    
    return {
        'model1_unique_analysis': {
            'count': len(model1_unique),
            'avg_distance': ...,
            'avg_iou': ...,
            'difficulty': ...  # 小目标、遮挡等
        },
        'model2_unique_analysis': {
            ...
        }
    }
```

### 2. 可视化差异

生成可视化图表展示：
- 共同匹配 vs 独有匹配的分布
- 不同距离/横向位置的共同匹配率
- 独有匹配的特征分析

## 实现优先级

1. **P0（必须）**：保存详细匹配信息
2. **P0（必须）**：找出共同匹配并重新计算统计
3. **P1（重要）**：生成基于共同匹配的对比报告
4. **P2（可选）**：分析独有匹配特征
5. **P3（可选）**：可视化工具

## 兼容性

- **向后兼容**：默认不启用，保持现有行为
- **可选启用**：通过 `--use-common-matches` 标志启用
- **存储开销**：详细匹配文件约为评测报告的 5-10 倍大小
