287 lines
8.2 KiB
Markdown
287 lines
8.2 KiB
Markdown
|
|
# 两级路径下重复 Case 名称问题修复
|
|||
|
|
|
|||
|
|
## 问题描述
|
|||
|
|
|
|||
|
|
在使用两级路径结构(`path_depth: 2`)时,发现:
|
|||
|
|
- **数据加载阶段**: 找到 155 个 cases
|
|||
|
|
- **评测阶段**: 只处理了 59 个 cases
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Found 155 case(s) in detection root: ... (path_depth=2)
|
|||
|
|
Processing case [1/59]: seq-03 (1278 frames)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 问题原因
|
|||
|
|
|
|||
|
|
在两级路径结构下,不同的 level1 目录可能包含相同名称的 case:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
det_root/
|
|||
|
|
dataset_A/
|
|||
|
|
seq-03/ ← 同名 case
|
|||
|
|
seq-27/
|
|||
|
|
dataset_B/
|
|||
|
|
seq-03/ ← 同名 case
|
|||
|
|
seq-28/
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
原有代码在评测阶段按 case 名称分组时,只使用了 `case_name` 作为键:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# 原有代码(有问题)
|
|||
|
|
cases = {}
|
|||
|
|
for pair in self.image_pairs:
|
|||
|
|
case_name = pair['case'] # 只使用 case 名称
|
|||
|
|
if case_name not in cases:
|
|||
|
|
cases[case_name] = []
|
|||
|
|
cases[case_name].append(pair)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
这导致:
|
|||
|
|
- `dataset_A/seq-03` 和 `dataset_B/seq-03` 都被归到 `cases['seq-03']` 中
|
|||
|
|
- 两个不同的 case 被合并成一个
|
|||
|
|
- 155 个实际 case 被合并成 59 个唯一名称的 case
|
|||
|
|
|
|||
|
|
## 解决方案
|
|||
|
|
|
|||
|
|
### 修改分组逻辑
|
|||
|
|
|
|||
|
|
在 `evaluate_2d()` 和 `evaluate_3d()` 方法中,使用唯一的 case 标识符进行分组:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# 修复后的代码
|
|||
|
|
cases = {}
|
|||
|
|
for pair in self.image_pairs:
|
|||
|
|
# 创建唯一的 case 标识符
|
|||
|
|
level1_name = pair.get('level1_name')
|
|||
|
|
case_name = pair['case']
|
|||
|
|
if level1_name:
|
|||
|
|
case_key = f"{level1_name}/{case_name}" # 两级路径: "dataset_A/seq-03"
|
|||
|
|
else:
|
|||
|
|
case_key = case_name # 一级路径: "seq-03"
|
|||
|
|
|
|||
|
|
if case_key not in cases:
|
|||
|
|
cases[case_key] = []
|
|||
|
|
cases[case_key].append(pair)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 更新循环变量
|
|||
|
|
|
|||
|
|
将循环中的 `case_name` 改为 `case_key`,并在所有相关位置使用:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# 修复前
|
|||
|
|
for case_idx, (case_name, case_pairs) in enumerate(cases.items(), 1):
|
|||
|
|
print(f"Processing case [{case_idx}/{len(cases)}]: {case_name} ...")
|
|||
|
|
self.per_case_metrics_2d[case_name] = ...
|
|||
|
|
|
|||
|
|
# 修复后
|
|||
|
|
for case_idx, (case_key, case_pairs) in enumerate(cases.items(), 1):
|
|||
|
|
print(f"Processing case [{case_idx}/{len(cases)}]: {case_key} ...")
|
|||
|
|
self.per_case_metrics_2d[case_key] = ...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 修改的文件
|
|||
|
|
|
|||
|
|
**eval_tools/evaluator/evaluator.py**
|
|||
|
|
|
|||
|
|
修改了三个方法:
|
|||
|
|
1. `evaluate_2d()` - 2D 评测的分组逻辑(第 494-556 行)
|
|||
|
|
2. `evaluate_3d()` - 3D 评测的分组逻辑(第 580-662 行)
|
|||
|
|
3. `_write_per_case_reports()` - Per-case 报告生成,添加文件名安全处理(第 862-864 行)
|
|||
|
|
|
|||
|
|
主要改动:
|
|||
|
|
- 第 494-507 行:2D 评测的分组和循环
|
|||
|
|
- 第 556 行:保存 per-case 结果时使用 `case_key`
|
|||
|
|
- 第 580-596 行:3D 评测的分组和循环
|
|||
|
|
- 第 627、636、640、662 行:保存 detailed matches 时使用 `case_key`
|
|||
|
|
- 第 862-864 行:生成报告文件时将 "/" 替换为 "_"
|
|||
|
|
|
|||
|
|
## 测试验证
|
|||
|
|
|
|||
|
|
创建了测试脚本 `eval_tools/tests/test_duplicate_case_names.py`:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
python eval_tools/tests/test_duplicate_case_names.py
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
测试结果:
|
|||
|
|
```
|
|||
|
|
============================================================
|
|||
|
|
Test Summary
|
|||
|
|
============================================================
|
|||
|
|
✓ TEST PASSED
|
|||
|
|
|
|||
|
|
The fix correctly handles duplicate case names across
|
|||
|
|
different level1 directories by using unique case identifiers.
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
测试验证了:
|
|||
|
|
1. 数据加载阶段正确识别所有 cases(包括重名的)
|
|||
|
|
2. 评测阶段正确处理所有 cases(不会合并重名的)
|
|||
|
|
3. Per-case 报告使用唯一标识符
|
|||
|
|
|
|||
|
|
## 预期效果
|
|||
|
|
|
|||
|
|
修复后,运行评测时:
|
|||
|
|
|
|||
|
|
### 修复前
|
|||
|
|
```
|
|||
|
|
Found 155 case(s) in detection root: ... (path_depth=2)
|
|||
|
|
Processing case [1/59]: seq-03 (1278 frames) ← 合并了多个同名 case
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 修复后
|
|||
|
|
```
|
|||
|
|
Found 155 case(s) in detection root: ... (path_depth=2)
|
|||
|
|
Processing case [1/155]: dataset_A/seq-03 (640 frames)
|
|||
|
|
Processing case [2/155]: dataset_B/seq-03 (638 frames) ← 正确分开
|
|||
|
|
Processing case [3/155]: dataset_A/seq-27 (512 frames)
|
|||
|
|
...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Case 标识符格式
|
|||
|
|
|
|||
|
|
### 一级路径(`path_depth: 1`)
|
|||
|
|
- Case 标识符: `seq-03`
|
|||
|
|
- 显示格式: `seq-03`
|
|||
|
|
|
|||
|
|
### 两级路径(`path_depth: 2`)
|
|||
|
|
- Case 标识符: `dataset_A/seq-03`
|
|||
|
|
- 显示格式: `dataset_A/seq-03`
|
|||
|
|
|
|||
|
|
## Per-Case 报告
|
|||
|
|
|
|||
|
|
Per-case 报告文件名会将 "/" 替换为 "_" 以兼容文件系统:
|
|||
|
|
|
|||
|
|
### 一级路径
|
|||
|
|
```
|
|||
|
|
per_case_reports/
|
|||
|
|
seq-03_report.txt
|
|||
|
|
seq-27_report.txt
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 两级路径
|
|||
|
|
```
|
|||
|
|
per_case_reports/
|
|||
|
|
dataset_A_seq-03_report.txt ← "/" 被替换为 "_"
|
|||
|
|
dataset_B_seq-03_report.txt
|
|||
|
|
dataset_A_seq-27_report.txt
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**重要**: 由于文件系统不允许文件名中包含 "/",在生成报告文件时,`case_key` 中的 "/" 会被自动替换为 "_"。这个转换在 `_write_per_case_reports()` 方法中自动完成:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# 在 evaluator.py 中
|
|||
|
|
for case_name in sorted(case_names):
|
|||
|
|
# 将 "/" 替换为 "_" 以兼容文件系统
|
|||
|
|
safe_case_name = case_name.replace('/', '_')
|
|||
|
|
case_report_path = os.path.join(per_case_dir, f'{safe_case_name}_report.txt')
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 向后兼容性
|
|||
|
|
|
|||
|
|
- 对于一级路径结构(`path_depth: 1`),`level1_name` 为 `None`,`case_key` 就是 `case_name`
|
|||
|
|
- 行为与之前完全相同,不会有任何变化
|
|||
|
|
- 所有现有配置和脚本无需修改
|
|||
|
|
|
|||
|
|
## 使用示例
|
|||
|
|
|
|||
|
|
### 示例 1: 查看评测输出
|
|||
|
|
|
|||
|
|
修复前(合并了重名 case):
|
|||
|
|
```
|
|||
|
|
Processing case [1/59]: seq-03 (1278 frames)
|
|||
|
|
seq-03: 100%|████████| 1278/1278 [00:05<00:00, 245.67it/s]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
修复后(正确分开):
|
|||
|
|
```
|
|||
|
|
Processing case [1/155]: dataset_A/seq-03 (640 frames)
|
|||
|
|
dataset_A/seq-03: 100%|████████| 640/640 [00:02<00:00, 248.12it/s]
|
|||
|
|
|
|||
|
|
Processing case [2/155]: dataset_B/seq-03 (638 frames)
|
|||
|
|
dataset_B/seq-03: 100%|████████| 638/638 [00:02<00:00, 246.89it/s]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 示例 2: 查看 Per-Case 报告
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 列出所有 per-case 报告
|
|||
|
|
ls evaluation_results/*/per_case_reports/
|
|||
|
|
|
|||
|
|
# 输出(修复后):
|
|||
|
|
dataset_A_seq-03_report.txt
|
|||
|
|
dataset_A_seq-27_report.txt
|
|||
|
|
dataset_B_seq-03_report.txt
|
|||
|
|
dataset_B_seq-28_report.txt
|
|||
|
|
...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 示例 3: 查看评测报告 JSON
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
import json
|
|||
|
|
|
|||
|
|
with open('evaluation_results/.../evaluation_report.json', 'r') as f:
|
|||
|
|
report = json.load(f)
|
|||
|
|
|
|||
|
|
# 查看 per-case 2D 结果
|
|||
|
|
for case_key, metrics in report['per_case_2d'].items():
|
|||
|
|
print(f"{case_key}: mAP={metrics['overall']['map']:.4f}")
|
|||
|
|
|
|||
|
|
# 输出(修复后):
|
|||
|
|
# dataset_A/seq-03: mAP=0.8234
|
|||
|
|
# dataset_A/seq-27: mAP=0.8156
|
|||
|
|
# dataset_B/seq-03: mAP=0.8312
|
|||
|
|
# dataset_B/seq-28: mAP=0.8089
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 故障排查
|
|||
|
|
|
|||
|
|
### 问题:仍然看到 case 数量不匹配
|
|||
|
|
|
|||
|
|
**检查项**:
|
|||
|
|
1. 确认已经更新到最新代码
|
|||
|
|
2. 确认 `path_depth: 2` 已设置
|
|||
|
|
3. 检查是否有其他原因导致 case 被跳过(如缺少文件)
|
|||
|
|
|
|||
|
|
**调试方法**:
|
|||
|
|
```python
|
|||
|
|
# 在 load_data_from_paths 后添加调试输出
|
|||
|
|
print(f"Total image pairs: {len(evaluator.image_pairs)}")
|
|||
|
|
print(f"Unique cases: {len(set(pair['case'] for pair in evaluator.image_pairs))}")
|
|||
|
|
print(f"Unique case_keys: {len(set(f\"{pair.get('level1_name', '')}/{pair['case']}\" for pair in evaluator.image_pairs))}")
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 问题:Per-case 报告文件名包含特殊字符
|
|||
|
|
|
|||
|
|
**原因**: `case_key` 中的 "/" 在文件名中不合法,会被操作系统误认为是目录分隔符
|
|||
|
|
|
|||
|
|
**错误示例**:
|
|||
|
|
```python
|
|||
|
|
# 错误:会尝试创建 per_case_reports/20251115/seq-03_report.txt
|
|||
|
|
case_report_path = os.path.join(per_case_dir, f'{case_key}_report.txt')
|
|||
|
|
# 如果 case_key = "20251115/seq-03",会导致 FileNotFoundError
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**解决方案**: 代码会自动将 "/" 替换为 "_"
|
|||
|
|
```python
|
|||
|
|
# 正确:创建 per_case_reports/20251115_seq-03_report.txt
|
|||
|
|
safe_case_name = case_key.replace('/', '_')
|
|||
|
|
case_report_path = os.path.join(per_case_dir, f'{safe_case_name}_report.txt')
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**修复位置**: `evaluator.py` 第 862-864 行
|
|||
|
|
|
|||
|
|
## 相关文档
|
|||
|
|
|
|||
|
|
- [TWO_LEVEL_PATH_SUPPORT.md](TWO_LEVEL_PATH_SUPPORT.md) - 两级路径功能说明
|
|||
|
|
- [CALIBRATION_FILE_FIX_CN.md](CALIBRATION_FILE_FIX_CN.md) - 校准文件问题修复
|
|||
|
|
|
|||
|
|
## 总结
|
|||
|
|
|
|||
|
|
此修复确保了评测系统在两级路径结构下能够正确处理重复的 case 名称,通过使用唯一的 case 标识符(`level1/case`)来区分不同 level1 目录下的同名 case。修改保持了向后兼容性,不影响现有的一级路径结构使用。
|
|||
|
|
|
|||
|
|
修复后,评测结果将更加准确,每个 case 都会被独立评测和报告,不会因为名称重复而被错误地合并。
|