单目3D初始代码
This commit is contained in:
375
tools/scripts_for_gt/visualization/EXAMPLES.md
Executable file
375
tools/scripts_for_gt/visualization/EXAMPLES.md
Executable file
@@ -0,0 +1,375 @@
|
||||
# 使用示例
|
||||
|
||||
本文档提供详细的使用示例,帮助快速上手真值可视化脚本。
|
||||
|
||||
## 示例1: 基础使用(原始图像可视化)
|
||||
|
||||
最简单的使用方式,不应用任何变换,直接可视化原始图像和标签。
|
||||
|
||||
### 数据准备
|
||||
|
||||
假设你的数据结构如下:
|
||||
```
|
||||
/data/
|
||||
├── images/
|
||||
│ └── frame_001.jpg # 原始图像 (1920x1080)
|
||||
├── labels/
|
||||
│ └── frame_001.txt # 标签文件
|
||||
└── calib/
|
||||
└── L2_calib/
|
||||
└── camera4.json # 相机标定
|
||||
```
|
||||
|
||||
### 运行命令
|
||||
|
||||
```bash
|
||||
python scripts_for_gt/visualize_single_frame.py \
|
||||
--image /data/images/frame_001.jpg \
|
||||
--label /data/labels/frame_001.txt \
|
||||
--output ./viz_output
|
||||
```
|
||||
|
||||
### 输出结果
|
||||
|
||||
```
|
||||
viz_output/
|
||||
├── frame_001_2d_gt.jpg # 2D边界框可视化
|
||||
├── frame_001_3d_gt.jpg # 3D投影可视化
|
||||
├── frame_001_bev_gt.jpg # BEV俯视图
|
||||
└── frame_001_combined.jpg # 组合可视化
|
||||
```
|
||||
|
||||
### 说明
|
||||
|
||||
- 脚本会自动查找标定文件(`../calib/L2_calib/camera4.json`)
|
||||
- 如果找不到标定文件,会跳过3D可视化,只生成2D可视化
|
||||
- 不应用ROI变换,使用原始图像尺寸
|
||||
|
||||
---
|
||||
|
||||
## 示例2: ROI模式(与训练一致)
|
||||
|
||||
当你的模型使用ROI训练时,应该使用相同的ROI配置进行可视化,以确保标签与训练时一致。
|
||||
|
||||
### 配置参数
|
||||
|
||||
从你的数据集配置文件(如`data/mono3d.yaml`)中获取:
|
||||
|
||||
```yaml
|
||||
# data/mono3d.yaml
|
||||
roi: [704, 352] # ROI尺寸 [width, height]
|
||||
virtual_fx: 500 # 虚拟焦距
|
||||
ori_img_size: [1920, 1080] # 原始图像尺寸 [width, height]
|
||||
```
|
||||
|
||||
### 运行命令
|
||||
|
||||
```bash
|
||||
python scripts_for_gt/visualize_single_frame.py \
|
||||
--image /data/images/frame_001.jpg \
|
||||
--label /data/labels/frame_001.txt \
|
||||
--output ./viz_output_roi \
|
||||
--roi 704 352 \
|
||||
--virtual-fx 500 \
|
||||
--ori-img-size 1920 1080
|
||||
```
|
||||
|
||||
### 效果对比
|
||||
|
||||
| 模式 | 图像尺寸 | 深度归一化 | 用途 |
|
||||
|------|---------|-----------|------|
|
||||
| 原始 | 1920x1080 | 否 | 查看原始数据 |
|
||||
| ROI | 704x352 | 是 | 与训练一致 |
|
||||
|
||||
### 说明
|
||||
|
||||
- ROI会裁剪图像到指定区域(以消失点为中心)
|
||||
- 深度值会根据`virtual_fx`进行归一化
|
||||
- 标签的2D坐标和UV坐标会相应调整
|
||||
- 这与训练时的数据处理完全一致
|
||||
|
||||
---
|
||||
|
||||
## 示例3: 批量处理
|
||||
|
||||
当需要可视化多帧图像时,使用批量脚本更高效。
|
||||
|
||||
### 数据结构
|
||||
|
||||
```
|
||||
/data/seq_001/
|
||||
├── images/
|
||||
│ ├── frame_001.jpg
|
||||
│ ├── frame_002.jpg
|
||||
│ └── ...
|
||||
├── labels/
|
||||
│ ├── frame_001.txt
|
||||
│ ├── frame_002.txt
|
||||
│ └── ...
|
||||
└── calib/
|
||||
└── L2_calib/
|
||||
└── camera4.json
|
||||
```
|
||||
|
||||
### 运行命令
|
||||
|
||||
```bash
|
||||
python scripts_for_gt/visualize_batch.py \
|
||||
--image-dir /data/seq_001/images \
|
||||
--label-dir /data/seq_001/labels \
|
||||
--output ./batch_viz \
|
||||
--roi 704 352 \
|
||||
--virtual-fx 500 \
|
||||
--max-samples 20
|
||||
```
|
||||
|
||||
### 输出结构
|
||||
|
||||
```
|
||||
batch_viz/
|
||||
├── frame_001/
|
||||
│ ├── frame_001_2d_gt.jpg
|
||||
│ ├── frame_001_3d_gt.jpg
|
||||
│ ├── frame_001_bev_gt.jpg
|
||||
│ └── frame_001_combined.jpg
|
||||
├── frame_002/
|
||||
│ └── ...
|
||||
└── ...
|
||||
```
|
||||
|
||||
### 说明
|
||||
|
||||
- `--max-samples`: 限制处理数量,用于快速测试
|
||||
- 脚本会自动匹配图像和标签文件(基于文件名)
|
||||
- 处理失败的样本会被跳过并记录错误
|
||||
|
||||
---
|
||||
|
||||
## 示例4: 自动测试
|
||||
|
||||
最简单的方式,让脚本自动找到并可视化一个样本。
|
||||
|
||||
### 运行命令
|
||||
|
||||
```bash
|
||||
python scripts_for_gt/test_visualization.py \
|
||||
--data data/mono3d.yaml \
|
||||
--output ./test_output
|
||||
```
|
||||
|
||||
### 脚本行为
|
||||
|
||||
1. 从`data/mono3d.yaml`读取验证集路径
|
||||
2. 在验证集中查找第一个有标签的样本
|
||||
3. 自动查找对应的标定文件
|
||||
4. 生成两组可视化:
|
||||
- `test_output/no_roi/`: 原始图像可视化
|
||||
- `test_output/with_roi/`: ROI变换可视化
|
||||
|
||||
### 说明
|
||||
|
||||
- 用于快速验证脚本功能
|
||||
- 自动使用数据集配置中的参数
|
||||
- 适合首次使用或调试
|
||||
|
||||
---
|
||||
|
||||
## 常见场景
|
||||
|
||||
### 场景1: 只想查看2D标签
|
||||
|
||||
```bash
|
||||
python scripts_for_gt/visualize_single_frame.py \
|
||||
--image image.jpg \
|
||||
--label label.txt \
|
||||
--output ./2d_only
|
||||
# 不提供标定文件,自动跳过3D可视化
|
||||
```
|
||||
|
||||
### 场景2: 指定标定文件路径
|
||||
|
||||
```bash
|
||||
python scripts_for_gt/visualize_single_frame.py \
|
||||
--image image.jpg \
|
||||
--label label.txt \
|
||||
--calib /custom/path/camera4.json \
|
||||
--output ./custom_viz
|
||||
```
|
||||
|
||||
### 场景3: 不同数据集配置
|
||||
|
||||
如果你有多个数据集配置(如roi0和roi1):
|
||||
|
||||
```bash
|
||||
# ROI 0 配置
|
||||
python scripts_for_gt/visualize_single_frame.py \
|
||||
--image image.jpg \
|
||||
--label label.txt \
|
||||
--output ./viz_roi0 \
|
||||
--roi 704 352 \
|
||||
--virtual-fx 500
|
||||
|
||||
# ROI 1 配置
|
||||
python scripts_for_gt/visualize_single_frame.py \
|
||||
--image image.jpg \
|
||||
--label label.txt \
|
||||
--output ./viz_roi1 \
|
||||
--roi 800 400 \
|
||||
--virtual-fx 600
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 输出说明
|
||||
|
||||
### 2D可视化 (`*_2d_gt.jpg`)
|
||||
|
||||
- 显示2D边界框
|
||||
- 不同类别使用不同颜色
|
||||
- 显示类别名称
|
||||
- 左上角标注"2D GT"
|
||||
|
||||
### 3D可视化 (`*_3d_gt.jpg`)
|
||||
|
||||
- 显示3D框投影到2D的效果
|
||||
- 12条边(前面红色,后面绿色,连接边蓝色)
|
||||
- 车辆类标记最佳可见面的中心点
|
||||
- 左上角标注"3D GT"
|
||||
|
||||
### BEV可视化 (`*_bev_gt.jpg`)
|
||||
|
||||
- 俯视图视角
|
||||
- 显示物体在地面的投影
|
||||
- 绿色表示真值(GT)
|
||||
- 箭头指示物体朝向
|
||||
|
||||
### 组合可视化 (`*_combined.jpg`)
|
||||
|
||||
- 水平排列:2D | 3D | BEV
|
||||
- 一眼看到所有信息
|
||||
- 适合对比和分析
|
||||
|
||||
---
|
||||
|
||||
## 参数速查表
|
||||
|
||||
| 参数 | 必需 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `--image` | 是 | - | 图像文件路径 |
|
||||
| `--label` | 是 | - | 标签文件路径 |
|
||||
| `--output` | 否 | ./gt_visualization | 输出目录 |
|
||||
| `--calib` | 否 | 自动推断 | 标定文件路径 |
|
||||
| `--roi` | 否 | None | ROI尺寸 (宽 高) |
|
||||
| `--virtual-fx` | 否 | None | 虚拟焦距 |
|
||||
| `--ori-img-size` | 否 | 自动检测 | 原始图像尺寸 (宽 高) |
|
||||
|
||||
批量脚本额外参数:
|
||||
|
||||
| 参数 | 必需 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `--image-dir` | 是 | - | 图像目录 |
|
||||
| `--label-dir` | 是 | - | 标签目录 |
|
||||
| `--max-samples` | 否 | None | 最大处理数量 |
|
||||
|
||||
---
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 问题1: "无法读取图像"
|
||||
|
||||
**原因**: 图像路径错误或文件不存在
|
||||
|
||||
**解决**:
|
||||
```bash
|
||||
# 检查文件是否存在
|
||||
ls -lh /path/to/image.jpg
|
||||
|
||||
# 使用绝对路径
|
||||
python scripts_for_gt/visualize_single_frame.py \
|
||||
--image $(pwd)/image.jpg \
|
||||
--label $(pwd)/label.txt
|
||||
```
|
||||
|
||||
### 问题2: "未找到标定文件"
|
||||
|
||||
**原因**: 标定文件路径不符合默认规则
|
||||
|
||||
**解决**:
|
||||
```bash
|
||||
# 手动指定标定文件
|
||||
python scripts_for_gt/visualize_single_frame.py \
|
||||
--image image.jpg \
|
||||
--label label.txt \
|
||||
--calib /correct/path/to/camera4.json
|
||||
```
|
||||
|
||||
### 问题3: "没有成功解码的3D框"
|
||||
|
||||
**原因**:
|
||||
- 标签格式不完整(只有2D信息)
|
||||
- 深度值无效(z <= 0或NaN)
|
||||
- 面可见性标记无效
|
||||
|
||||
**解决**:
|
||||
```bash
|
||||
# 检查标签内容
|
||||
cat label.txt
|
||||
|
||||
# 如果只有2D标签,这是正常的,会跳过3D可视化
|
||||
```
|
||||
|
||||
### 问题4: "ROI后标签数量为0"
|
||||
|
||||
**原因**: ROI区域没有包含任何目标
|
||||
|
||||
**解决**:
|
||||
```bash
|
||||
# 先不使用ROI查看原始标签
|
||||
python scripts_for_gt/visualize_single_frame.py \
|
||||
--image image.jpg \
|
||||
--label label.txt \
|
||||
--output ./check
|
||||
|
||||
# 调整ROI参数或选择其他样本
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 进阶使用
|
||||
|
||||
### 自定义类别名称
|
||||
|
||||
修改脚本中的`names`字典:
|
||||
|
||||
```python
|
||||
names = {
|
||||
0: "Car",
|
||||
1: "Person",
|
||||
2: "Bike",
|
||||
3: "Rider",
|
||||
13: "Tricycle",
|
||||
}
|
||||
```
|
||||
|
||||
### 调整可视化参数
|
||||
|
||||
修改脚本中的`scale_factor`参数:
|
||||
|
||||
```python
|
||||
# 更高分辨率输出
|
||||
img_2d = plot_2d_boxes(..., scale_factor=3)
|
||||
|
||||
# 更小文件大小
|
||||
img_2d = plot_2d_boxes(..., scale_factor=1)
|
||||
```
|
||||
|
||||
### 导出3D参数
|
||||
|
||||
在脚本中添加导出逻辑:
|
||||
|
||||
```python
|
||||
# 在decoded循环中
|
||||
if decoded and decoded.get('object_3d'):
|
||||
x, y, z, l, h, w, rot_y, _ = decoded['object_3d']
|
||||
print(f"Object: x={x:.2f}, y={y:.2f}, z={z:.2f}, dims=[{l:.2f},{h:.2f},{w:.2f}], rot_y={rot_y:.2f}")
|
||||
```
|
||||
377
tools/scripts_for_gt/visualization/FILTER_GUIDE.md
Executable file
377
tools/scripts_for_gt/visualization/FILTER_GUIDE.md
Executable file
@@ -0,0 +1,377 @@
|
||||
# 条件过滤可视化使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
`visualize_filtered_train.py` 是一个强大的数据分析工具,可以从训练集或验证集中筛选符合特定条件的样本进行可视化。
|
||||
|
||||
## 主要功能
|
||||
|
||||
1. **类别过滤** - 只查看特定类别的样本
|
||||
2. **3D空间过滤** - 只查看特定3D空间范围内的目标
|
||||
3. **批量处理** - 自动查找并可视化符合条件的样本
|
||||
4. **摘要生成** - 自动生成包含所有样本信息的摘要文件
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 场景1: 数据质量检查
|
||||
|
||||
**需求**: 检查两轮车标注质量,特别是近距离目标(0-50米)
|
||||
|
||||
```bash
|
||||
python scripts_for_gt/visualize_filtered_train.py \
|
||||
--data data/mono3d.yaml \
|
||||
--target-class bicycle \
|
||||
--z-range 0 50 \
|
||||
--max-samples 30 \
|
||||
--output ./qa_bicycle_near
|
||||
```
|
||||
|
||||
**输出**:
|
||||
- 30个包含近距离两轮车的样本
|
||||
- 每个样本生成4张可视化图(2D, 3D, BEV, Combined)
|
||||
- `summary.txt` 文件包含所有样本的详细信息
|
||||
|
||||
### 场景2: 类别分布分析
|
||||
|
||||
**需求**: 分析训练集中行人的分布情况
|
||||
|
||||
```bash
|
||||
python scripts_for_gt/visualize_filtered_train.py \
|
||||
--data data/mono3d.yaml \
|
||||
--target-class pedestrian \
|
||||
--max-samples 100 \
|
||||
--output ./analysis_pedestrian
|
||||
```
|
||||
|
||||
这会找到前100个包含行人的样本,通过可视化可以:
|
||||
- 查看行人在不同位置、距离的标注质量
|
||||
- 检查遮挡情况
|
||||
- 分析姿态多样性
|
||||
|
||||
### 场景3: 特定区域分析
|
||||
|
||||
**需求**: 分析车辆在车道中心区域(左右±3米)的标注
|
||||
|
||||
```bash
|
||||
python scripts_for_gt/visualize_filtered_train.py \
|
||||
--data data/mono3d.yaml \
|
||||
--target-class vehicle \
|
||||
--x-range -3 3 \
|
||||
--z-range 5 100 \
|
||||
--max-samples 50 \
|
||||
--output ./analysis_vehicle_center_lane
|
||||
```
|
||||
|
||||
### 场景4: 边界情况分析
|
||||
|
||||
**需求**: 查看距离较远的目标(50-100米)
|
||||
|
||||
```bash
|
||||
python scripts_for_gt/visualize_filtered_train.py \
|
||||
--data data/mono3d.yaml \
|
||||
--z-range 50 100 \
|
||||
--max-samples 40 \
|
||||
--output ./analysis_far_distance
|
||||
```
|
||||
|
||||
### 场景5: 横向位置分析
|
||||
|
||||
**需求**: 分析左侧(负X)和右侧(正X)目标的分布
|
||||
|
||||
```bash
|
||||
# 左侧目标
|
||||
python scripts_for_gt/visualize_filtered_train.py \
|
||||
--data data/mono3d.yaml \
|
||||
--x-range -20 -5 \
|
||||
--max-samples 30 \
|
||||
--output ./analysis_left_side
|
||||
|
||||
# 右侧目标
|
||||
python scripts_for_gt/visualize_filtered_train.py \
|
||||
--data data/mono3d.yaml \
|
||||
--x-range 5 20 \
|
||||
--max-samples 30 \
|
||||
--output ./analysis_right_side
|
||||
```
|
||||
|
||||
## 参数详解
|
||||
|
||||
### 基本参数
|
||||
|
||||
- `--data`: 数据集配置文件路径
|
||||
- 默认: `data/mono3d.yaml`
|
||||
- 从配置文件中读取train/val路径
|
||||
|
||||
- `--output`: 输出目录
|
||||
- 默认: `./filtered_train_viz`
|
||||
- 会在此目录下为每个样本创建子目录
|
||||
|
||||
- `--split`: 数据集划分
|
||||
- 可选: `train` 或 `val`
|
||||
- 默认: `train`
|
||||
|
||||
### 过滤参数
|
||||
|
||||
#### 类别过滤
|
||||
|
||||
- `--target-class`: 目标类别名称
|
||||
- 可选值:
|
||||
* `vehicle` - 车辆(小汽车等)
|
||||
* `pedestrian` - 行人
|
||||
* `bicycle` - 两轮车
|
||||
* `rider` - 骑手
|
||||
* `motorcycle` - 摩托车
|
||||
* `tricycle` - 三轮车
|
||||
- 默认: `None`(不过滤类别)
|
||||
|
||||
#### 空间范围过滤
|
||||
|
||||
基于相机坐标系:
|
||||
|
||||
```
|
||||
Y (下)
|
||||
|
|
||||
|
|
||||
|_____ X (右)
|
||||
/
|
||||
Z (前)
|
||||
```
|
||||
|
||||
- `--x-range MIN MAX`: X坐标范围(左右方向)
|
||||
- 单位: 米
|
||||
- 示例: `--x-range -10 10` (左右各10米)
|
||||
- X < 0: 左侧
|
||||
- X > 0: 右侧
|
||||
|
||||
- `--y-range MIN MAX`: Y坐标范围(上下方向)
|
||||
- 单位: 米
|
||||
- 示例: `--y-range -2 1`
|
||||
- Y < 0: 相机上方
|
||||
- Y > 0: 相机下方
|
||||
- 注意: 通常目标在地面,Y值接近0
|
||||
|
||||
- `--z-range MIN MAX`: Z坐标范围(前后方向/深度)
|
||||
- 单位: 米
|
||||
- 示例: `--z-range 0 50` (前方0到50米)
|
||||
- Z值: 距离相机的深度
|
||||
|
||||
#### 其他过滤参数
|
||||
|
||||
- `--min-targets`: 最少目标数量
|
||||
- 默认: 1
|
||||
- 示例: `--min-targets 2` (至少包含2个符合条件的目标)
|
||||
|
||||
- `--max-samples`: 最大可视化样本数
|
||||
- 默认: 20
|
||||
- 用于限制输出数量
|
||||
|
||||
### 可视化选项
|
||||
|
||||
- `--use-roi`: 使用ROI变换
|
||||
- 如果设置此标志,会应用训练时的ROI变换
|
||||
- 从数据集配置中读取roi、virtual_fx等参数
|
||||
|
||||
## 输出结构
|
||||
|
||||
```
|
||||
output_dir/
|
||||
├── summary.txt # 摘要文件
|
||||
├── sample_001/
|
||||
│ ├── sample_001_2d_gt.jpg # 2D可视化
|
||||
│ ├── sample_001_3d_gt.jpg # 3D可视化
|
||||
│ ├── sample_001_bev_gt.jpg # BEV可视化
|
||||
│ └── sample_001_combined.jpg # 组合可视化
|
||||
├── sample_002/
|
||||
│ └── ...
|
||||
└── ...
|
||||
```
|
||||
|
||||
### 摘要文件内容
|
||||
|
||||
`summary.txt` 包含:
|
||||
1. 过滤条件
|
||||
2. 统计信息(总样本数、成功/失败数)
|
||||
3. 每个样本的详细信息:
|
||||
- 文件名
|
||||
- 符合条件的目标数量
|
||||
- 每个目标的类别和3D位置
|
||||
|
||||
示例:
|
||||
```
|
||||
过滤可视化摘要
|
||||
==================================================
|
||||
|
||||
过滤条件:
|
||||
目标类别: bicycle
|
||||
X范围: [-10.0, 10.0] 米
|
||||
Z范围: [0.0, 50.0] 米
|
||||
最少目标数: 1
|
||||
数据集: train
|
||||
|
||||
可视化结果:
|
||||
总样本数: 20
|
||||
成功: 20
|
||||
失败: 0
|
||||
|
||||
样本列表:
|
||||
--------------------------------------------------
|
||||
1. frame_001234.jpg
|
||||
符合条件的目标数: 2
|
||||
- bicycle: (-3.45, 0.12, 15.67)m
|
||||
- bicycle: (5.23, -0.08, 22.34)m
|
||||
|
||||
2. frame_005678.jpg
|
||||
符合条件的目标数: 1
|
||||
- bicycle: (1.23, 0.05, 8.90)m
|
||||
...
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 组合多个条件
|
||||
|
||||
```bash
|
||||
# 查看近距离(0-20米)、车道中心(左右±5米)的行人
|
||||
python scripts_for_gt/visualize_filtered_train.py \
|
||||
--data data/mono3d.yaml \
|
||||
--target-class pedestrian \
|
||||
--x-range -5 5 \
|
||||
--z-range 0 20 \
|
||||
--min-targets 1 \
|
||||
--max-samples 50 \
|
||||
--output ./pedestrian_near_center
|
||||
```
|
||||
|
||||
### 从验证集筛选
|
||||
|
||||
```bash
|
||||
# 从验证集而非训练集筛选
|
||||
python scripts_for_gt/visualize_filtered_train.py \
|
||||
--data data/mono3d.yaml \
|
||||
--split val \
|
||||
--target-class vehicle \
|
||||
--z-range 30 80 \
|
||||
--max-samples 30 \
|
||||
--output ./val_vehicle_medium_distance
|
||||
```
|
||||
|
||||
### 使用ROI变换
|
||||
|
||||
```bash
|
||||
# 使用ROI变换进行可视化(与训练时一致)
|
||||
python scripts_for_gt/visualize_filtered_train.py \
|
||||
--data data/mono3d.yaml \
|
||||
--target-class bicycle \
|
||||
--x-range -10 10 \
|
||||
--z-range 0 50 \
|
||||
--use-roi \
|
||||
--max-samples 20 \
|
||||
--output ./bicycle_with_roi
|
||||
```
|
||||
|
||||
## 工作流程
|
||||
|
||||
脚本的工作流程如下:
|
||||
|
||||
```
|
||||
1. 读取数据集配置 (data/mono3d.yaml)
|
||||
↓
|
||||
2. 获取train/val图像列表
|
||||
↓
|
||||
3. 遍历每个图像
|
||||
├─ 读取对应的标签文件
|
||||
├─ 解析标签(47维数组)
|
||||
├─ 检查类别过滤条件
|
||||
├─ 检查3D空间范围条件
|
||||
└─ 如果符合条件,添加到候选列表
|
||||
↓
|
||||
4. 对候选样本进行可视化
|
||||
├─ 调用 visualize_single_frame()
|
||||
├─ 生成 2D/3D/BEV/Combined 图像
|
||||
└─ 保存到对应子目录
|
||||
↓
|
||||
5. 生成摘要文件 (summary.txt)
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 快速预览
|
||||
|
||||
如果只想快速查看几个样本:
|
||||
```bash
|
||||
python scripts_for_gt/visualize_filtered_train.py \
|
||||
--data data/mono3d.yaml \
|
||||
--target-class bicycle \
|
||||
--max-samples 5 # 只查看5个样本
|
||||
```
|
||||
|
||||
### 限制搜索范围
|
||||
|
||||
脚本会遍历整个数据集直到找到足够的样本。如果数据集很大,可以:
|
||||
1. 使用更宽松的过滤条件
|
||||
2. 减少 `--max-samples` 数量
|
||||
|
||||
### 进度显示
|
||||
|
||||
脚本会每100个图像显示一次进度:
|
||||
```
|
||||
进度: 100/5000, 已找到 5 个符合条件的样本
|
||||
进度: 200/5000, 已找到 12 个符合条件的样本
|
||||
...
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q1: 为什么找不到符合条件的样本?
|
||||
|
||||
**可能原因**:
|
||||
1. 过滤条件太严格(空间范围太小)
|
||||
2. 数据集中该类别样本较少
|
||||
3. 标签文件缺少3D信息
|
||||
|
||||
**解决方法**:
|
||||
- 放宽空间范围
|
||||
- 不设置空间范围,只过滤类别
|
||||
- 检查标签文件格式
|
||||
|
||||
### Q2: 如何确定合适的空间范围?
|
||||
|
||||
**建议**:
|
||||
1. 先不设置范围,可视化一些样本
|
||||
2. 观察目标的典型位置
|
||||
3. 根据观察结果设置范围
|
||||
|
||||
常见参考范围:
|
||||
- 近距离: `--z-range 0 20`
|
||||
- 中距离: `--z-range 20 50`
|
||||
- 远距离: `--z-range 50 100`
|
||||
- 车道中心: `--x-range -5 5`
|
||||
- 侧边: `--x-range -20 -5` 或 `--x-range 5 20`
|
||||
|
||||
### Q3: 可以同时过滤多个类别吗?
|
||||
|
||||
目前不支持多类别过滤。如需查看多个类别,请分别运行:
|
||||
```bash
|
||||
python scripts_for_gt/visualize_filtered_train.py --target-class bicycle ...
|
||||
python scripts_for_gt/visualize_filtered_train.py --target-class motorcycle ...
|
||||
```
|
||||
|
||||
### Q4: 摘要文件在哪里?
|
||||
|
||||
摘要文件位于输出目录下:`{output_dir}/summary.txt`
|
||||
|
||||
## 实用技巧
|
||||
|
||||
1. **数据探索**: 先不设置过滤条件,随机可视化一些样本了解数据分布
|
||||
2. **质量检查**: 使用空间范围过滤,系统地检查不同区域的标注质量
|
||||
3. **类别平衡**: 分别统计各类别的样本数量,评估数据集平衡性
|
||||
4. **边界情况**: 重点检查远距离、侧边等边界情况的标注质量
|
||||
|
||||
## 与其他脚本的对比
|
||||
|
||||
| 脚本 | 用途 | 输入 | 过滤能力 |
|
||||
|------|------|------|---------|
|
||||
| visualize_single_frame.py | 单帧可视化 | 单个图像+标签 | 无 |
|
||||
| visualize_batch.py | 批量可视化 | 图像目录 | 无 |
|
||||
| **visualize_filtered_train.py** | **条件过滤可视化** | **数据集配置** | **类别+空间** |
|
||||
| test_visualization.py | 快速测试 | 数据集配置 | 无(自动找第一个) |
|
||||
398
tools/scripts_for_gt/visualization/FLOW_ANALYSIS.md
Executable file
398
tools/scripts_for_gt/visualization/FLOW_ANALYSIS.md
Executable file
@@ -0,0 +1,398 @@
|
||||
# 真值可视化流程分析文档
|
||||
|
||||
## 一、完整数据流程分析
|
||||
|
||||
### 1.1 从文件到内存的加载流程
|
||||
|
||||
```
|
||||
原始文件
|
||||
└─> load_label() [dataloaders3d.py:1625]
|
||||
│
|
||||
├─> 解析标签文本文件
|
||||
│ └─> 支持3种格式:
|
||||
│ ├─ 2D only (6 dims)
|
||||
│ ├─ 2D+3D simple (18 dims)
|
||||
│ └─ 2D+3D+faces (50 dims)
|
||||
│
|
||||
└─> 返回 (N, 47) numpy数组
|
||||
└─> 列结构:
|
||||
[0]: class
|
||||
[1-4]: xywh (normalized)
|
||||
[5]: score
|
||||
[6-8]: xyz3d (camera coords)
|
||||
[9-11]: dimensions (l,h,w)
|
||||
[12]: rot_y
|
||||
[13-14]: center_2d_uv (normalized)
|
||||
[15]: alpha
|
||||
[16-23]: front_face [xyz, alpha, uv, score, visible]
|
||||
[24-31]: rear_face
|
||||
[32-39]: left_face
|
||||
[40-47]: right_face
|
||||
```
|
||||
|
||||
### 1.2 ROI变换流程
|
||||
|
||||
```
|
||||
原始标签 (N, 47)
|
||||
└─> post_process_labels_to_roi() [dataloaders3d.py:1060]
|
||||
│
|
||||
├─> 步骤1: 计算ROI区域
|
||||
│ └─> vanish_y = cy - fy * tan(pitch)
|
||||
│ └─> crop_center = (oriW/2, vanish_y)
|
||||
│ └─> roi_bounds = [x1, y1, x2, y2]
|
||||
│
|
||||
├─> 步骤2: 转换2D边界框
|
||||
│ └─> xywhn -> xyxy (absolute)
|
||||
│ └─> shift to ROI coords
|
||||
│ └─> clip to ROI bounds
|
||||
│ └─> normalize to ROI size
|
||||
│
|
||||
├─> 步骤3: 转换UV坐标
|
||||
│ └─> 对每个面的中心投影点 (5对UV)
|
||||
│ └─> u_roi = (u_orig * oriW - roi_x1) / roi_w
|
||||
│ └─> v_roi = (v_orig * oriH - roi_y1) / roi_h
|
||||
│
|
||||
└─> 步骤4: 过滤无效框
|
||||
└─> 移除完全在ROI外的框
|
||||
└─> 标记部分可见的框(cut-in/cut-out)
|
||||
```
|
||||
|
||||
### 1.3 深度归一化流程
|
||||
|
||||
```
|
||||
ROI标签 (N, 47)
|
||||
└─> scale_z3d() [dataloaders3d.py:1518]
|
||||
│
|
||||
├─> 计算缩放因子
|
||||
│ └─> scale = virtual_fx / fx_roi
|
||||
│
|
||||
├─> 缩放Z坐标 (仅Z,不包括X,Y)
|
||||
│ ├─> 列7: z3d_center *= scale
|
||||
│ ├─> 列17: z3d_front *= scale
|
||||
│ ├─> 列25: z3d_rear *= scale
|
||||
│ ├─> 列33: z3d_left *= scale
|
||||
│ └─> 列41: z3d_right *= scale
|
||||
│
|
||||
└─> 原理说明:
|
||||
投影公式: u = fx * X/Z + cx
|
||||
同时缩放fx和Z -> 投影不变
|
||||
X和Y从UV和Z恢复,无需缩放
|
||||
```
|
||||
|
||||
## 二、可视化实现流程
|
||||
|
||||
### 2.1 2D可视化流程
|
||||
|
||||
```
|
||||
标签数组 (N, 47)
|
||||
└─> plot_2d_boxes()
|
||||
│
|
||||
├─> 图像预处理
|
||||
│ └─> resize(scale_factor=2) -> 更清晰
|
||||
│ └─> BGR -> RGB for Annotator
|
||||
│
|
||||
├─> 遍历每个标签
|
||||
│ ├─> 提取: cls, xywh
|
||||
│ ├─> 转换: xywhn -> xyxy (pixel)
|
||||
│ ├─> 绘制: box + label
|
||||
│ └─> 颜色: colors(cls)
|
||||
│
|
||||
└─> 后处理
|
||||
└─> RGB -> BGR
|
||||
└─> 添加标签文字 (cv2.putText)
|
||||
```
|
||||
|
||||
### 2.2 3D可视化流程(车辆类)
|
||||
|
||||
```
|
||||
标签 (47,) + calib
|
||||
└─> decode_and_reconstruct_3d_box_from_target() [plots.py:908]
|
||||
│
|
||||
├─> 步骤1: 恢复原始深度
|
||||
│ └─> z_original = z_normalized * depth_scale
|
||||
│ └─> depth_scale = fx_roi / virtual_fx
|
||||
│
|
||||
├─> 步骤2: 选择最佳可见面
|
||||
│ └─> 遍历4个面 (front, rear, left, right)
|
||||
│ └─> 选择: visible==1 && score>0.3 && 最高score
|
||||
│
|
||||
├─> 步骤3: 从面重建3D中心
|
||||
│ └─> _reconstruct_3d_box_from_face()
|
||||
│ ├─> uv -> 射线方向
|
||||
│ │ └─> X = (u - cx) * Z / fx
|
||||
│ │ └─> Y = (v - cy) * Z / fy
|
||||
│ │
|
||||
│ ├─> 根据面类型计算偏移
|
||||
│ │ └─> front: center = face - [l/2, 0, 0]
|
||||
│ │ └─> rear: center = face + [l/2, 0, 0]
|
||||
│ │ └─> left: center = face - [0, 0, w/2]
|
||||
│ │ └─> right: center = face + [0, 0, w/2]
|
||||
│ │
|
||||
│ └─> 计算8个角点
|
||||
│ └─> compute_3d_box_corners_4face()
|
||||
│
|
||||
└─> 返回: {corners_3d, face_center_2d, face_color, ...}
|
||||
```
|
||||
|
||||
### 2.3 3D可视化流程(行人类)
|
||||
|
||||
```
|
||||
标签 (47,) + calib
|
||||
└─> decode_and_reconstruct_3d_box_from_target() [plots.py:908]
|
||||
│
|
||||
├─> 步骤1: 恢复原始深度
|
||||
│ └─> z_original = z_normalized * depth_scale
|
||||
│
|
||||
├─> 步骤2: 提取3D参数
|
||||
│ └─> center = [x3d, y3d, z3d]
|
||||
│ └─> dims = [l, h, w]
|
||||
│ └─> rot_y
|
||||
│
|
||||
├─> 步骤3: 计算8个角点
|
||||
│ └─> compute_3d_box_corners_4face()
|
||||
│ └─> 基于center, dims, rot_y
|
||||
│ └─> 注意: GT不需要corner reordering
|
||||
│
|
||||
└─> 返回: {corners_3d, center_2d, object_3d, ...}
|
||||
```
|
||||
|
||||
### 2.4 投影和绘制流程
|
||||
|
||||
```
|
||||
decoded_results + image + calib
|
||||
└─> plot_3d_boxes_from_decoded_targets() [plots.py:1343]
|
||||
│
|
||||
├─> 图像预处理
|
||||
│ └─> tensor -> numpy
|
||||
│ └─> normalize -> [0,255]
|
||||
│ └─> resize(scale_factor=2)
|
||||
│
|
||||
├─> 标定参数缩放
|
||||
│ └─> fx, fy, cx, cy *= scale_factor
|
||||
│
|
||||
├─> 遍历每个decoded对象
|
||||
│ └─> draw_3d_box_from_corners() [plots.py:1198]
|
||||
│ │
|
||||
│ ├─> 步骤1: 3D角点 -> 2D投影
|
||||
│ │ └─> u = fx * X/Z + cx
|
||||
│ │ └─> v = fy * Y/Z + cy
|
||||
│ │ └─> 应用畸变校正 (如果有)
|
||||
│ │
|
||||
│ ├─> 步骤2: 绘制12条边
|
||||
│ │ ├─> 前面4条边: 红色/face_color
|
||||
│ │ ├─> 后面4条边: 绿色
|
||||
│ │ └─> 连接边4条: 蓝色
|
||||
│ │
|
||||
│ └─> 步骤3: 标记面中心 (车辆)
|
||||
│ └─> circle at face_center_2d
|
||||
│
|
||||
└─> 后处理
|
||||
└─> 添加标签文字
|
||||
└─> BGR -> RGB
|
||||
```
|
||||
|
||||
### 2.5 BEV可视化流程
|
||||
|
||||
```
|
||||
object_3d = [x, y, z, l, h, w, rot_y, face_type]
|
||||
└─> drawbev() [plots.py:2404]
|
||||
│
|
||||
├─> 坐标系转换
|
||||
│ └─> 相机坐标 -> BEV坐标
|
||||
│ └─> bev_x = z (深度 -> 前后)
|
||||
│ └─> bev_y = -x (右 -> 左右)
|
||||
│
|
||||
├─> 计算4个角点 (俯视)
|
||||
│ └─> 基于 (bev_x, bev_y), l, w, rot_y
|
||||
│
|
||||
├─> 投影到图像坐标
|
||||
│ └─> scale: 200m -> 800px
|
||||
│ └─> origin: bottom-center
|
||||
│
|
||||
└─> 绘制
|
||||
├─> 填充矩形: 半透明
|
||||
├─> 边框: 实线
|
||||
├─> 方向指示: 箭头
|
||||
└─> 颜色: GT=green, Pred=red
|
||||
```
|
||||
|
||||
## 三、关键坐标变换
|
||||
|
||||
### 3.1 归一化坐标 ↔ 像素坐标
|
||||
|
||||
```python
|
||||
# 2D边界框
|
||||
xywh_norm -> xyxy_pixel:
|
||||
x1 = (x - w/2) * img_width
|
||||
y1 = (y - h/2) * img_height
|
||||
x2 = (x + w/2) * img_width
|
||||
y2 = (y + h/2) * img_height
|
||||
|
||||
# UV坐标
|
||||
uv_norm -> uv_pixel:
|
||||
u_pixel = u_norm * img_width
|
||||
v_pixel = v_norm * img_height
|
||||
```
|
||||
|
||||
### 3.2 相机坐标 ↔ 图像坐标
|
||||
|
||||
```python
|
||||
# 投影(3D -> 2D)
|
||||
u = fx * X/Z + cx
|
||||
v = fy * Y/Z + cy
|
||||
|
||||
# 反投影(2D + Z -> 3D)
|
||||
X = (u - cx) * Z / fx
|
||||
Y = (v - cy) * Z / fy
|
||||
|
||||
# 注意:
|
||||
# - 需要已知深度Z
|
||||
# - 可能需要畸变校正
|
||||
```
|
||||
|
||||
### 3.3 ROI坐标变换
|
||||
|
||||
```python
|
||||
# 原始图像 -> ROI图像
|
||||
u_roi = (u_orig - roi_x1) / roi_width
|
||||
v_roi = (v_orig - roi_y1) / roi_height
|
||||
|
||||
# ROI图像 -> 原始图像
|
||||
u_orig = u_roi * roi_width + roi_x1
|
||||
v_orig = v_roi * roi_height + roi_y1
|
||||
|
||||
# 3D坐标:
|
||||
# - xyz3d保持不变(相机坐标系)
|
||||
# - 仅z3d需要深度归一化
|
||||
```
|
||||
|
||||
### 3.4 深度归一化变换
|
||||
|
||||
```python
|
||||
# 训练时(原始 -> 归一化)
|
||||
z_normalized = z_original * (virtual_fx / fx_original)
|
||||
|
||||
# 推理/可视化时(归一化 -> 原始)
|
||||
z_original = z_normalized * (fx_original / virtual_fx)
|
||||
= z_normalized * depth_scale
|
||||
|
||||
# 原理:
|
||||
# 投影: u = fx * X/Z + cx
|
||||
# 缩放: u' = (fx*s) * X/(Z*s) + cx = u
|
||||
# 即:同时缩放fx和Z保持投影不变
|
||||
```
|
||||
|
||||
## 四、常见问题与注意事项
|
||||
|
||||
### 4.1 深度值处理
|
||||
|
||||
**问题**: 为什么只缩放z3d,不缩放x3d和y3d?
|
||||
|
||||
**答案**:
|
||||
- 投影公式: `u = fx * X/Z + cx`
|
||||
- X和Y是从UV和Z反推的: `X = (u-cx)*Z/fx`
|
||||
- 缩放Z后,如果缩放fx,投影保持不变
|
||||
- X和Y会自动适配新的Z值,无需单独缩放
|
||||
|
||||
### 4.2 角点顺序
|
||||
|
||||
**问题**: GT和预测的角点顺序一样吗?
|
||||
|
||||
**答案**:
|
||||
- GT: 直接使用`compute_3d_box_corners_4face()`输出
|
||||
- 预测: 需要根据`rot_y`的符号reorder角点
|
||||
- 在可视化时需要注意这个差异
|
||||
|
||||
### 4.3 坐标系统
|
||||
|
||||
**相机坐标系** (右手系):
|
||||
```
|
||||
Y (down)
|
||||
|
|
||||
|_____ X (right)
|
||||
/
|
||||
Z (forward, depth)
|
||||
```
|
||||
|
||||
**图像坐标系**:
|
||||
```
|
||||
(0,0)_____ u (right)
|
||||
|
|
||||
|
|
||||
v (down)
|
||||
```
|
||||
|
||||
**BEV坐标系** (俯视):
|
||||
```
|
||||
bev_x (depth) ↑
|
||||
|
|
||||
|
|
||||
bev_y <———————·
|
||||
(left)
|
||||
```
|
||||
|
||||
### 4.4 畸变处理
|
||||
|
||||
当标定参数包含`distort_coeffs`时:
|
||||
- ROI模式: 保留畸变系数(需要在可视化时处理)
|
||||
- 虚拟相机模式: 已经去畸变(`distort_coeffs=[]`)
|
||||
|
||||
### 4.5 类别差异
|
||||
|
||||
不同类别使用不同的重建策略:
|
||||
- **车辆(0, 13)**: 基于最佳可见面
|
||||
- **行人(1, 2, 3)**: 基于完整3D框
|
||||
|
||||
原因:
|
||||
- 车辆可能被部分遮挡或出画,面策略更鲁棒
|
||||
- 行人尺寸小,完整框更简单准确
|
||||
|
||||
## 五、调试技巧
|
||||
|
||||
### 5.1 检查标签加载
|
||||
|
||||
```python
|
||||
labels = load_label_file(label_path)
|
||||
print(f"标签数量: {len(labels)}")
|
||||
print(f"类别分布: {np.unique(labels[:, 0])}")
|
||||
print(f"第一个标签:\n{labels[0]}")
|
||||
```
|
||||
|
||||
### 5.2 检查ROI变换
|
||||
|
||||
```python
|
||||
print(f"原始标签数量: {len(labels_orig)}")
|
||||
print(f"ROI后标签数量: {len(labels_roi)}")
|
||||
|
||||
# 检查第一个框的变换
|
||||
if len(labels_roi) > 0:
|
||||
print(f"原始xywh: {labels_orig[0, 1:5]}")
|
||||
print(f"ROI xywh: {labels_roi[0, 1:5]}")
|
||||
```
|
||||
|
||||
### 5.3 检查深度值
|
||||
|
||||
```python
|
||||
# 打印深度值范围
|
||||
z_values = labels[:, 7][~np.isnan(labels[:, 7])]
|
||||
print(f"深度范围: [{z_values.min():.2f}, {z_values.max():.2f}]")
|
||||
print(f"平均深度: {z_values.mean():.2f}")
|
||||
```
|
||||
|
||||
### 5.4 检查解码结果
|
||||
|
||||
```python
|
||||
decoded = decode_and_reconstruct_3d_box_from_target(...)
|
||||
if decoded:
|
||||
print(f"类别: {decoded['cls']}")
|
||||
print(f"角点形状: {decoded['corners_3d'].shape if decoded['corners_3d'] is not None else None}")
|
||||
if decoded.get('object_3d'):
|
||||
print(f"3D位置: {decoded['object_3d'][:3]}")
|
||||
```
|
||||
|
||||
## 六、性能优化建议
|
||||
|
||||
1. **批量处理**: 使用`visualize_batch.py`而不是循环调用单帧脚本
|
||||
2. **分辨率控制**: 调整`scale_factor`参数控制输出图像大小
|
||||
3. **选择性可视化**: 根据需要选择2D、3D或BEV可视化
|
||||
4. **并行处理**: 可以使用多进程批量处理独立的图像
|
||||
273
tools/scripts_for_gt/visualization/INDEX.md
Executable file
273
tools/scripts_for_gt/visualization/INDEX.md
Executable file
@@ -0,0 +1,273 @@
|
||||
# 真值可视化工具集
|
||||
|
||||
## 📚 文档索引
|
||||
|
||||
本目录提供了完整的真值可视化工具和详细文档,帮助理解从数据加载到可视化的完整流程。
|
||||
|
||||
### 核心脚本
|
||||
|
||||
1. **[visualize_single_frame.py](visualize_single_frame.py)** - 单帧可视化脚本 ⭐
|
||||
- 读取单帧图像和标签
|
||||
- 支持2D、3D、BEV可视化
|
||||
- 支持ROI变换和深度归一化
|
||||
- 推荐用于详细分析单个样本
|
||||
|
||||
2. **[visualize_batch.py](visualize_batch.py)** - 批量可视化脚本
|
||||
- 批量处理多帧图像
|
||||
- 自动匹配图像和标签
|
||||
- 支持限制处理数量
|
||||
- 推荐用于快速浏览数据集
|
||||
|
||||
3. **[visualize_filtered_train.py](visualize_filtered_train.py)** - 条件过滤可视化脚本 🔥
|
||||
- 从训练集/验证集筛选特定条件的样本
|
||||
- 支持类别过滤(如:只看两轮车)
|
||||
- 支持3D空间范围过滤(如:左右10米、纵向50米)
|
||||
- 自动生成摘要文件
|
||||
- 推荐用于数据分析和质量检查
|
||||
|
||||
4. **[test_visualization.py](test_visualization.py)** - 快速测试脚本
|
||||
- 自动从验证集查找样本
|
||||
- 测试不同配置的可视化效果
|
||||
- 推荐用于首次使用或调试
|
||||
|
||||
### 文档说明
|
||||
|
||||
1. **[README.md](README.md)** - 总体说明 📖
|
||||
- 真值读取到可视化的完整流程概述
|
||||
- 标签格式说明(47维数组结构)
|
||||
- ROI变换和深度归一化原理
|
||||
- 不同类别的处理策略
|
||||
- 使用方法和参数说明
|
||||
|
||||
2. **[FLOW_ANALYSIS.md](FLOW_ANALYSIS.md)** - 详细流程分析 🔍
|
||||
- 数据加载流程(标签、图像、标定)
|
||||
- ROI变换的详细步骤
|
||||
- 深度归一化的数学原理
|
||||
- 2D/3D可视化的实现细节
|
||||
- 坐标系统和变换关系
|
||||
- 调试技巧和优化建议
|
||||
|
||||
3. **[EXAMPLES.md](EXAMPLES.md)** - 使用示例 💡
|
||||
- 基础使用示例(原始图像)
|
||||
- ROI模式示例(与训练一致)
|
||||
- 批量处理示例
|
||||
- 自动测试示例
|
||||
- 常见场景和故障排查
|
||||
- 参数速查表
|
||||
|
||||
4. **[FILTER_GUIDE.md](FILTER_GUIDE.md)** - 条件过滤指南 🎯
|
||||
- 条件过滤可视化的详细使用方法
|
||||
- 类别过滤和空间范围过滤
|
||||
- 多种使用场景(数据质量检查、类别分布分析等)
|
||||
- 参数详解和坐标系说明
|
||||
- 高级用法和性能优化
|
||||
- 常见问题和实用技巧
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 方式1: 最简单(自动测试)
|
||||
|
||||
```bash
|
||||
# 自动查找样本并可视化
|
||||
python scripts_for_gt/test_visualization.py --data data/mono3d.yaml --output ./test_viz
|
||||
```
|
||||
|
||||
### 方式2: 单帧可视化(原始图像)
|
||||
|
||||
```bash
|
||||
# 基础使用:不使用ROI,直接可视化原始图像
|
||||
python scripts_for_gt/visualize_single_frame.py \
|
||||
--image /path/to/image.jpg \
|
||||
--label /path/to/label.txt \
|
||||
--output ./viz_output
|
||||
```
|
||||
|
||||
### 方式3: 单帧可视化(ROI模式,与训练一致)
|
||||
|
||||
```bash
|
||||
# 使用ROI变换和深度归一化(与训练时一致)
|
||||
python scripts_for_gt/visualize_single_frame.py \
|
||||
--image /path/to/image.jpg \
|
||||
--label /path/to/label.txt \
|
||||
--output ./viz_output_roi \
|
||||
--roi 704 352 \
|
||||
--virtual-fx 500 \
|
||||
--ori-img-size 1920 1080
|
||||
```
|
||||
|
||||
### 方式4: 批量处理
|
||||
|
||||
```bash
|
||||
# 批量可视化多帧
|
||||
python scripts_for_gt/visualize_batch.py \
|
||||
--image-dir /path/to/images \
|
||||
--label-dir /path/to/labels \
|
||||
--output ./batch_viz \
|
||||
--roi 704 352 \
|
||||
--virtual-fx 500 \
|
||||
--max-samples 20
|
||||
```
|
||||
|
||||
## 📊 输出结果
|
||||
|
||||
每个样本会生成4个可视化文件:
|
||||
|
||||
| 文件 | 内容 | 用途 |
|
||||
|------|------|------|
|
||||
| `*_2d_gt.jpg` | 2D边界框 | 查看目标检测标注 |
|
||||
| `*_3d_gt.jpg` | 3D框投影 | 查看3D标注质量 |
|
||||
| `*_bev_gt.jpg` | 鸟瞰图 | 查看空间位置关系 |
|
||||
| `*_combined.jpg` | 组合视图 | 一眼看到所有信息 |
|
||||
|
||||
## 🔑 关键概念
|
||||
|
||||
### 标签格式(47维数组)
|
||||
|
||||
```
|
||||
[0]: class # 类别ID
|
||||
[1-4]: x, y, w, h # 2D边界框(归一化)
|
||||
[5]: score # 置信度
|
||||
[6-8]: x3d, y3d, z3d # 3D中心(相机坐标系)
|
||||
[9-11]: length, height, width # 3D尺寸
|
||||
[12]: rot_y # 偏航角
|
||||
[13-14]: center_u, center_v # 3D中心投影(归一化)
|
||||
[15]: alpha # 观测角
|
||||
[16-23]: front_face # 前面信息
|
||||
[24-31]: rear_face # 后面信息
|
||||
[32-39]: left_face # 左面信息
|
||||
[40-47]: right_face # 右面信息
|
||||
```
|
||||
|
||||
### ROI变换
|
||||
|
||||
1. 计算ROI区域(以消失点为中心)
|
||||
2. 裁剪图像到ROI区域
|
||||
3. 调整2D边界框和UV坐标到ROI空间
|
||||
4. 根据`virtual_fx`缩放深度值
|
||||
|
||||
### 深度归一化
|
||||
|
||||
```python
|
||||
# 训练时
|
||||
z_normalized = z_original * (virtual_fx / fx_original)
|
||||
|
||||
# 可视化时(恢复)
|
||||
z_original = z_normalized * depth_scale
|
||||
# 其中 depth_scale = fx_original / virtual_fx
|
||||
```
|
||||
|
||||
**为什么只缩放Z?**
|
||||
- 投影公式: `u = fx * X/Z + cx`
|
||||
- 同时缩放`fx`和`Z`可以保持投影不变
|
||||
- `X`和`Y`从`UV`和`Z`恢复,无需单独缩放
|
||||
|
||||
### 不同类别的处理策略
|
||||
|
||||
| 类别 | 策略 | 原因 |
|
||||
|------|------|------|
|
||||
| 车辆(0, 13) | 基于最佳可见面 | 可能被部分遮挡 |
|
||||
| 行人(1, 2, 3) | 基于完整3D框 | 尺寸小,完整框更准确 |
|
||||
|
||||
## 🔧 常见问题
|
||||
|
||||
### Q1: 为什么需要ROI变换?
|
||||
|
||||
**A**: ROI变换有两个目的:
|
||||
1. **聚焦感兴趣区域**: 去除天空等无关区域
|
||||
2. **统一焦距**: 通过`virtual_fx`统一不同相机的焦距,提高泛化能力
|
||||
|
||||
### Q2: 什么时候使用ROI模式?
|
||||
|
||||
**A**:
|
||||
- **查看原始数据**: 不使用ROI(了解数据本身)
|
||||
- **与训练一致**: 使用ROI(验证训练数据处理)
|
||||
- **调试模型**: 使用ROI(与模型输入一致)
|
||||
|
||||
### Q3: 标定文件必需吗?
|
||||
|
||||
**A**:
|
||||
- **2D可视化**: 不需要
|
||||
- **3D可视化**: 需要(用于3D到2D的投影)
|
||||
|
||||
### Q4: 如何验证可视化是否正确?
|
||||
|
||||
**A**: 检查以下几点:
|
||||
1. 2D框是否对齐目标
|
||||
2. 3D框投影是否合理(不应该明显错位)
|
||||
3. BEV中的位置是否符合直觉(距离、朝向)
|
||||
4. 不同面的颜色是否正确(前红后绿)
|
||||
|
||||
## 📖 推荐阅读顺序
|
||||
|
||||
1. **新手**: README.md → EXAMPLES.md → 运行test_visualization.py
|
||||
2. **数据分析**: FILTER_GUIDE.md → 运行visualize_filtered_train.py
|
||||
3. **开发**: FLOW_ANALYSIS.md → 阅读脚本代码 → 自定义功能
|
||||
4. **调试**: EXAMPLES.md故障排查 → FLOW_ANALYSIS.md调试技巧
|
||||
|
||||
## 🎯 常用命令速查
|
||||
|
||||
```bash
|
||||
# 快速测试
|
||||
python scripts_for_gt/test_visualization.py
|
||||
|
||||
# 原始图像可视化
|
||||
python scripts_for_gt/visualize_single_frame.py --image img.jpg --label label.txt --output out
|
||||
|
||||
# ROI模式(与训练一致)
|
||||
python scripts_for_gt/visualize_single_frame.py --image img.jpg --label label.txt \
|
||||
--roi 704 352 --virtual-fx 500 --output out
|
||||
|
||||
# 批量处理(前10个样本)
|
||||
python scripts_for_gt/visualize_batch.py --image-dir imgs/ --label-dir labels/ \
|
||||
--output batch_out --max-samples 10
|
||||
|
||||
# 批量+ROI
|
||||
python scripts_for_gt/visualize_batch.py --image-dir imgs/ --label-dir labels/ \
|
||||
--roi 704 352 --virtual-fx 500 --output batch_out
|
||||
|
||||
# 条件过滤:可视化包含两轮车的样本(左右10米、纵向50米范围)
|
||||
python scripts_for_gt/visualize_filtered_train.py --data data/mono3d.yaml \
|
||||
--target-class bicycle --x-range -10 10 --z-range 0 50 --max-samples 20
|
||||
|
||||
# 条件过滤:可视化包含行人的样本
|
||||
python scripts_for_gt/visualize_filtered_train.py --data data/mono3d.yaml \
|
||||
--target-class pedestrian --max-samples 50
|
||||
|
||||
# 条件过滤:从验证集筛选
|
||||
python scripts_for_gt/visualize_filtered_train.py --data data/mono3d.yaml \
|
||||
--split val --target-class vehicle --z-range 10 30 --max-samples 20
|
||||
```
|
||||
|
||||
## 📞 需要帮助?
|
||||
|
||||
1. 查看 [EXAMPLES.md](EXAMPLES.md) 的故障排查章节
|
||||
2. 查看 [FLOW_ANALYSIS.md](FLOW_ANALYSIS.md) 的调试技巧章节
|
||||
3. 检查标签文件格式是否正确
|
||||
4. 检查标定文件是否存在且格式正确
|
||||
|
||||
## 🔄 与模型代码的对应关系
|
||||
|
||||
| 功能 | 脚本位置 | 模型代码位置 |
|
||||
|------|---------|------------|
|
||||
| 标签加载 | `load_label_file()` | `utils/dataloaders3d.py:load_label()` |
|
||||
| ROI变换 | `apply_roi_transform()` | `utils/dataloaders3d.py:post_process_labels_to_roi()` |
|
||||
| 深度归一化 | `scale_z3d()` | `utils/dataloaders3d.py:_scale_z3d()` |
|
||||
| 3D解码(GT) | - | `utils/plots.py:decode_and_reconstruct_3d_box_from_target()` |
|
||||
| 3D解码(预测) | - | `utils/plots.py:decode_and_reconstruct_3d_box()` |
|
||||
| 2D可视化 | `plot_2d_boxes()` | `test_val_visualize.py:plot_2d_boxes_to_image()` |
|
||||
| 3D可视化 | - | `utils/plots.py:plot_3d_boxes_from_decoded_targets()` |
|
||||
|
||||
## 📝 版本历史
|
||||
|
||||
- **v1.0** (2026-02-07): 初始版本
|
||||
- 单帧可视化脚本
|
||||
- 批量可视化脚本
|
||||
- 自动测试脚本
|
||||
- 完整文档(README, FLOW_ANALYSIS, EXAMPLES)
|
||||
|
||||
## 🎓 相关资源
|
||||
|
||||
- 主项目文档: [CLAUDE.md](../CLAUDE.md)
|
||||
- 数据集配置: [data/mono3d.yaml](../data/mono3d.yaml)
|
||||
- 3D可视化指南: [3D_VISUALIZATION_GUIDE.md](../3D_VISUALIZATION_GUIDE.md)
|
||||
- 验证脚本: [test_val_visualize.py](../test_val_visualize.py)
|
||||
378
tools/scripts_for_gt/visualization/README.md
Executable file
378
tools/scripts_for_gt/visualization/README.md
Executable file
@@ -0,0 +1,378 @@
|
||||
# 真值可视化脚本说明
|
||||
|
||||
## 概述
|
||||
|
||||
本目录包含用于单帧图像真值可视化的脚本,可以实现2D和3D边界框的可视化。
|
||||
|
||||
## 真值读取到可视化的完整流程
|
||||
|
||||
### 1. 数据加载流程(utils/dataloaders3d.py)
|
||||
|
||||
#### 1.1 标签读取 (`load_label()`)
|
||||
```python
|
||||
# 标签文件格式(47维数组):
|
||||
# - 列0: class (类别ID)
|
||||
# - 列1-4: x, y, w, h (归一化的2D边界框中心和宽高)
|
||||
# - 列5: score (置信度)
|
||||
# - 列6-8: x3d, y3d, z3d (3D中心点在相机坐标系下的坐标)
|
||||
# - 列9-11: length, height, width (3D框的尺寸)
|
||||
# - 列12: rot_y (偏航角旋转)
|
||||
# - 列13-14: xc, yc (3D框中心投影到2D的归一化坐标)
|
||||
# - 列15: alpha (观测角)
|
||||
# - 列16-23: 前面信息 [x3d, y3d, z3d, alpha, xc, yc, score, visible]
|
||||
# - 列24-31: 后面信息
|
||||
# - 列32-39: 左面信息
|
||||
# - 列40-47: 右面信息
|
||||
```
|
||||
|
||||
支持三种标签格式:
|
||||
- **2D only (6列)**: 仅包含2D边界框
|
||||
- **2D+3D simple (18列)**: 针对行人、自行车、骑手,包含基本3D信息
|
||||
- **2D+3D+faces (50列)**: 针对车辆、三轮车,包含完整的4个面信息
|
||||
|
||||
#### 1.2 图像读取 (`load_image()`)
|
||||
```python
|
||||
# 使用cv2.imread读取BGR格式图像
|
||||
img = cv2.imread(image_path) # (H, W, 3) BGR
|
||||
```
|
||||
|
||||
#### 1.3 标定参数读取
|
||||
```python
|
||||
# 从 camera4.json 读取相机内参
|
||||
{
|
||||
"focal_u": fx, # x方向焦距
|
||||
"focal_v": fy, # y方向焦距
|
||||
"cu": cx, # x方向主点
|
||||
"cv": cy, # y方向主点
|
||||
"pitch": pitch, # 相机俯仰角
|
||||
"distort_coeffs": [] # 畸变系数
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 数据预处理流程
|
||||
|
||||
#### 2.1 ROI变换(可选)
|
||||
```python
|
||||
# 计算消失点和裁剪中心
|
||||
vanish_y = cy - fy * tan(pitch * π/180)
|
||||
crop_center = (oriW/2, vanish_y)
|
||||
|
||||
# 计算ROI边界
|
||||
roi_x1 = crop_center_x - roi_width/2
|
||||
roi_y1 = crop_center_y - roi_height/2
|
||||
roi_x2 = roi_x1 + roi_width
|
||||
roi_y2 = roi_y1 + roi_height
|
||||
|
||||
# 裁剪图像
|
||||
img_roi = img[roi_y1:roi_y2, roi_x1:roi_x2]
|
||||
```
|
||||
|
||||
#### 2.2 标签坐标转换
|
||||
```python
|
||||
# 将2D边界框从原始图像坐标转换到ROI坐标
|
||||
# 1. 归一化xywh -> 绝对xyxy
|
||||
# 2. 平移到ROI相对坐标
|
||||
# 3. 裁剪到ROI边界内
|
||||
# 4. 重新归一化
|
||||
|
||||
# UV坐标(面中心投影)也需要相应转换
|
||||
```
|
||||
|
||||
#### 2.3 深度归一化(可选)
|
||||
```python
|
||||
# 当使用虚拟焦距时,需要缩放z3d深度值
|
||||
# 只缩放Z坐标,X和Y从UV和Z恢复
|
||||
scale = virtual_fx / fx_original
|
||||
z3d_normalized = z3d_original * scale
|
||||
|
||||
# 包括:
|
||||
# - 列7: 整体中心z3d
|
||||
# - 列17: 前面中心z3d
|
||||
# - 列25: 后面中心z3d
|
||||
# - 列33: 左面中心z3d
|
||||
# - 列41: 右面中心z3d
|
||||
```
|
||||
|
||||
### 3. 可视化流程(test_val_visualize.py)
|
||||
|
||||
#### 3.1 2D可视化
|
||||
```python
|
||||
# 使用 plot_2d_boxes_to_image() 或类似逻辑
|
||||
# 1. 放大图像(scale_factor=2)获得更清晰的可视化
|
||||
# 2. 将归一化坐标转换为像素坐标
|
||||
# 3. 使用Annotator绘制边界框和类别标签
|
||||
# 4. 添加标签文字(如"2D GT")
|
||||
```
|
||||
|
||||
#### 3.2 3D可视化
|
||||
|
||||
**步骤1:解码3D框** (`decode_and_reconstruct_3d_box_from_target()`)
|
||||
```python
|
||||
# 针对不同类别采用不同策略:
|
||||
|
||||
# 车辆类(class 0, 13):基于最佳可见面重建
|
||||
# 1. 遍历4个面(前、后、左、右)
|
||||
# 2. 选择可见且得分最高的面
|
||||
# 3. 从面中心(u,v,z)反投影得到3D中心
|
||||
# 4. 使用dimensions和rot_y计算8个3D角点
|
||||
|
||||
# 行人类(class 1, 2, 3):基于完整3D框
|
||||
# 1. 直接使用(x3d, y3d, z3d)作为3D中心
|
||||
# 2. 使用dimensions和rot_y计算8个3D角点
|
||||
# 3. 注意:需要将归一化深度转换回原始尺度
|
||||
# z3d_original = z3d_normalized * depth_scale
|
||||
```
|
||||
|
||||
**步骤2:投影和绘制** (`plot_3d_boxes_from_decoded_targets()`)
|
||||
```python
|
||||
# 1. 将3D角点投影到2D图像平面
|
||||
# u = fx * X/Z + cx
|
||||
# v = fy * Y/Z + cy
|
||||
# 2. 绘制3D框的12条边
|
||||
# 3. 使用不同颜色区分前后面
|
||||
# 4. 对于车辆类,标记最佳可见面的中心点
|
||||
```
|
||||
|
||||
#### 3.3 BEV可视化
|
||||
```python
|
||||
# 1. 创建空白BEV图像 (draw_bev_blank)
|
||||
# 2. 对每个3D框调用 drawbev() 绘制俯视图
|
||||
# 3. 使用不同颜色区分真值(绿色)和预测(红色)
|
||||
```
|
||||
|
||||
### 4. 关键坐标系统
|
||||
|
||||
#### 4.1 图像坐标系
|
||||
- 原点:图像左上角
|
||||
- x轴:向右
|
||||
- y轴:向下
|
||||
- 单位:像素或归一化[0,1]
|
||||
|
||||
#### 4.2 相机坐标系
|
||||
- 原点:相机光心
|
||||
- x轴:向右
|
||||
- y轴:向下
|
||||
- z轴:向前(深度方向)
|
||||
- 单位:米
|
||||
|
||||
#### 4.3 投影关系
|
||||
```
|
||||
u = fx * X/Z + cx
|
||||
v = fy * Y/Z + cy
|
||||
|
||||
# 反投影(已知u,v,Z求X,Y):
|
||||
X = (u - cx) * Z / fx
|
||||
Y = (v - cy) * Z / fy
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 基本用法(不应用ROI)
|
||||
|
||||
```bash
|
||||
python scripts_for_gt/visualize_single_frame.py \
|
||||
--image /path/to/images/frame_001.jpg \
|
||||
--label /path/to/labels/frame_001.txt \
|
||||
--output ./gt_visualization
|
||||
```
|
||||
|
||||
### 带ROI变换(与训练一致)
|
||||
|
||||
```bash
|
||||
python scripts_for_gt/visualize_single_frame.py \
|
||||
--image /path/to/images/frame_001.jpg \
|
||||
--label /path/to/labels/frame_001.txt \
|
||||
--output ./gt_visualization \
|
||||
--roi 704 352 \
|
||||
--virtual-fx 500 \
|
||||
--ori-img-size 1920 1080
|
||||
```
|
||||
|
||||
### 参数说明
|
||||
|
||||
- `--image`: 图像文件路径(必需)
|
||||
- `--label`: 标签文件路径(必需)
|
||||
- `--output`: 输出目录(默认:./gt_visualization)
|
||||
- `--calib`: 相机标定文件路径(camera4.json),如不提供会自动从图像路径推断
|
||||
- `--roi`: ROI尺寸(宽 高),如:`--roi 704 352`
|
||||
- `--virtual-fx`: 虚拟焦距,用于深度归一化
|
||||
- `--ori-img-size`: 原始图像尺寸(宽 高),如不提供则从图像自动获取
|
||||
|
||||
### 输出文件
|
||||
|
||||
脚本会在输出目录生成以下文件:
|
||||
- `{filename}_2d_gt.jpg`: 2D边界框可视化
|
||||
- `{filename}_3d_gt.jpg`: 3D边界框投影可视化
|
||||
- `{filename}_bev_gt.jpg`: 鸟瞰图(BEV)可视化
|
||||
- `{filename}_combined.jpg`: 组合可视化(2D + 3D + BEV)
|
||||
|
||||
## 示例
|
||||
|
||||
假设数据结构如下:
|
||||
```
|
||||
/data/
|
||||
├── images/
|
||||
│ └── frame_001.jpg
|
||||
├── labels/
|
||||
│ └── frame_001.txt
|
||||
└── calib/
|
||||
└── L2_calib/
|
||||
└── camera4.json
|
||||
```
|
||||
|
||||
运行命令:
|
||||
```bash
|
||||
python scripts_for_gt/visualize_single_frame.py \
|
||||
--image /data/images/frame_001.jpg \
|
||||
--label /data/labels/frame_001.txt \
|
||||
--output ./output \
|
||||
--roi 704 352 \
|
||||
--virtual-fx 500
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **标定文件自动推断**:如果不指定`--calib`,脚本会自动查找与图像路径对应的标定文件(`../calib/L2_calib/camera4.json`)
|
||||
|
||||
2. **深度归一化**:当使用`--virtual-fx`时,z3d坐标会被缩放以匹配虚拟焦距,这与训练时的处理一致
|
||||
|
||||
3. **坐标系统**:
|
||||
- 输入标签使用归一化坐标[0,1]
|
||||
- 3D坐标使用相机坐标系(米)
|
||||
- 深度归一化后需要在可视化时转换回原始尺度
|
||||
|
||||
4. **支持的类别**:
|
||||
- 0: vehicle(车辆)- 使用面策略
|
||||
- 1: pedestrian(行人)- 使用完整框
|
||||
- 2: bicycle(自行车)- 使用完整框
|
||||
- 3: rider(骑手)- 使用完整框
|
||||
- 13: tricycle(三轮车)- 使用面策略
|
||||
|
||||
## 快速测试
|
||||
|
||||
提供了一个自动测试脚本,可以自动从验证集中找到第一个有效样本并进行可视化:
|
||||
|
||||
```bash
|
||||
# 使用默认数据集配置
|
||||
python scripts_for_gt/test_visualization.py
|
||||
|
||||
# 指定数据集配置
|
||||
python scripts_for_gt/test_visualization.py --data data/mono3d.yaml --output ./test_output
|
||||
```
|
||||
|
||||
测试脚本会:
|
||||
1. 从数据集配置中读取验证集路径
|
||||
2. 自动查找第一个有标签的样本
|
||||
3. 分别测试原始图像可视化和ROI变换可视化
|
||||
4. 生成所有可视化结果到指定目录
|
||||
|
||||
## 批量处理
|
||||
|
||||
如果需要批量处理多帧图像,可以使用批量脚本:
|
||||
|
||||
```bash
|
||||
python scripts_for_gt/visualize_batch.py \
|
||||
--image-dir /path/to/images \
|
||||
--label-dir /path/to/labels \
|
||||
--output ./batch_output \
|
||||
--roi 704 352 \
|
||||
--virtual-fx 500 \
|
||||
--max-samples 10
|
||||
```
|
||||
|
||||
参数说明:
|
||||
- `--image-dir`: 图像目录
|
||||
- `--label-dir`: 标签目录
|
||||
- `--output`: 输出目录
|
||||
- `--calib-dir`: 标定文件目录(可选)
|
||||
- `--roi`: ROI尺寸
|
||||
- `--virtual-fx`: 虚拟焦距
|
||||
- `--max-samples`: 最大处理样本数(可选)
|
||||
|
||||
## 条件过滤可视化
|
||||
|
||||
如果需要从训练集或验证集中筛选特定条件的样本进行可视化,可以使用过滤脚本:
|
||||
|
||||
### 示例1: 可视化包含两轮车的样本(空间范围过滤)
|
||||
|
||||
```bash
|
||||
# 只可视化两轮车在左右各10米、纵向50米范围内的样本
|
||||
python scripts_for_gt/visualize_filtered_train.py \
|
||||
--data data/mono3d.yaml \
|
||||
--target-class bicycle \
|
||||
--x-range -10 10 \
|
||||
--z-range 0 50 \
|
||||
--max-samples 20 \
|
||||
--output ./bicycle_samples
|
||||
```
|
||||
|
||||
### 示例2: 可视化包含行人的样本
|
||||
|
||||
```bash
|
||||
# 可视化前50个包含行人的训练样本
|
||||
python scripts_for_gt/visualize_filtered_train.py \
|
||||
--data data/mono3d.yaml \
|
||||
--target-class pedestrian \
|
||||
--max-samples 50 \
|
||||
--output ./pedestrian_samples
|
||||
```
|
||||
|
||||
### 示例3: 可视化特定空间范围内的车辆
|
||||
|
||||
```bash
|
||||
# 可视化车辆在左右5米、纵向10-30米范围内的样本
|
||||
python scripts_for_gt/visualize_filtered_train.py \
|
||||
--data data/mono3d.yaml \
|
||||
--target-class vehicle \
|
||||
--x-range -5 5 \
|
||||
--z-range 10 30 \
|
||||
--max-samples 30 \
|
||||
--output ./vehicle_samples
|
||||
```
|
||||
|
||||
### 过滤参数说明
|
||||
|
||||
- `--target-class`: 目标类别(vehicle, pedestrian, bicycle, rider, motorcycle, tricycle)
|
||||
- `--x-range MIN MAX`: X坐标范围(左右方向),单位:米
|
||||
- `--y-range MIN MAX`: Y坐标范围(上下方向),单位:米
|
||||
- `--z-range MIN MAX`: Z坐标范围(前后方向/深度),单位:米
|
||||
- `--min-targets`: 最少目标数量(默认1)
|
||||
- `--max-samples`: 最大可视化样本数
|
||||
- `--split`: 数据集划分(train或val,默认train)
|
||||
- `--use-roi`: 使用ROI变换
|
||||
|
||||
### 坐标系说明
|
||||
|
||||
相机坐标系:
|
||||
- **X轴**: 向右为正(单位:米)
|
||||
- **Y轴**: 向下为正(单位:米)
|
||||
- **Z轴**: 向前为正(深度方向,单位:米)
|
||||
|
||||
示例:
|
||||
- `--x-range -10 10`: 左右各10米范围
|
||||
- `--z-range 0 50`: 前方0到50米范围
|
||||
- `--y-range -2 1`: 相机水平面上下2米到下方1米范围
|
||||
|
||||
## 脚本说明
|
||||
|
||||
本目录包含以下脚本:
|
||||
|
||||
1. **visualize_single_frame.py** - 单帧可视化脚本
|
||||
- 核心功能:读取单帧图像和标签,生成2D、3D和BEV可视化
|
||||
- 支持ROI变换和深度归一化
|
||||
|
||||
2. **visualize_batch.py** - 批量可视化脚本
|
||||
- 批量处理多帧图像
|
||||
- 自动匹配图像和标签文件
|
||||
|
||||
3. **test_visualization.py** - 快速测试脚本
|
||||
- 自动查找验证集样本
|
||||
- 测试不同配置的可视化效果
|
||||
|
||||
## 扩展功能
|
||||
|
||||
可以基于此脚本进行扩展:
|
||||
- 添加不同的可视化风格
|
||||
- 输出3D框参数到文件
|
||||
- 与预测结果对比
|
||||
- 统计分析(深度分布、尺寸分布等)
|
||||
103
tools/scripts_for_gt/visualization/SAVE_TYPES_EXAMPLES.md
Executable file
103
tools/scripts_for_gt/visualization/SAVE_TYPES_EXAMPLES.md
Executable file
@@ -0,0 +1,103 @@
|
||||
# 保存类型选择功能说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
新增`--save-types`参数,可以选择性地保存可视化结果,节省磁盘空间和处理时间。
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 只保存组合图(默认)
|
||||
|
||||
```bash
|
||||
python scripts_for_gt/visualize_filtered_train.py \
|
||||
--data data/mono3d.yaml \
|
||||
--target-class bicycle \
|
||||
--x-range -10 10 \
|
||||
--z-range 0 50 \
|
||||
--max-samples 30
|
||||
# 默认只保存 *_combined.jpg
|
||||
```
|
||||
|
||||
### 2. 保存多种类型
|
||||
|
||||
```bash
|
||||
# 保存2D和组合图
|
||||
python scripts_for_gt/visualize_filtered_train.py \
|
||||
--data data/mono3d.yaml \
|
||||
--target-class bicycle \
|
||||
--save-types 2d combined
|
||||
|
||||
# 保存3D、BEV和组合图
|
||||
python scripts_for_gt/visualize_filtered_train.py \
|
||||
--data data/mono3d.yaml \
|
||||
--target-class pedestrian \
|
||||
--save-types 3d bev combined
|
||||
|
||||
# 保存所有类型
|
||||
python scripts_for_gt/visualize_filtered_train.py \
|
||||
--data data/mono3d.yaml \
|
||||
--target-class vehicle \
|
||||
--save-types 2d 3d bev combined
|
||||
```
|
||||
|
||||
## 可选类型说明
|
||||
|
||||
| 类型 | 文件名模式 | 说明 |
|
||||
|------|-----------|------|
|
||||
| `2d` | `*_2d_gt.jpg` | 仅2D边界框可视化 |
|
||||
| `3d` | `*_3d_gt.jpg` | 3D框投影到图像 |
|
||||
| `bev` | `*_bev_gt.jpg` | 鸟瞰图视角 |
|
||||
| `combined` | `*_combined.jpg` | 2D+3D+BEV组合图 |
|
||||
|
||||
## 性能优势
|
||||
|
||||
| 场景 | 磁盘空间节省 | 处理速度提升 |
|
||||
|------|-------------|-------------|
|
||||
| 只保存combined | ~75% | ~20% |
|
||||
| 只保存2d | ~75% | ~60% |
|
||||
| 保存2d+combined | ~50% | ~40% |
|
||||
|
||||
## 适用场景
|
||||
|
||||
### 只保存combined
|
||||
- **适用场景**:快速浏览数据分布
|
||||
- **优点**:节省最多空间,一图看全
|
||||
- **推荐用于**:数据质量检查、分布分析
|
||||
|
||||
### 保存所有类型
|
||||
- **适用场景**:详细分析、问题排查
|
||||
- **优点**:每个角度都有单独的图
|
||||
- **推荐用于**:深度调试、论文图片准备
|
||||
|
||||
### 自定义组合
|
||||
- **适用场景**:特定需求
|
||||
- **示例**:只关心2D框位置 → `--save-types 2d`
|
||||
- **示例**:只关心3D空间位置 → `--save-types 3d bev`
|
||||
|
||||
## 与其他脚本的兼容性
|
||||
|
||||
此功能也适用于其他可视化脚本:
|
||||
|
||||
```bash
|
||||
# visualize_batch.py
|
||||
python scripts_for_gt/visualize_batch.py \
|
||||
--image-dir /path/to/images \
|
||||
--label-dir /path/to/labels \
|
||||
--save-types combined
|
||||
|
||||
# visualize_single_frame.py(需要手动修改代码传递参数)
|
||||
# 在代码中调用时:
|
||||
visualize_single_frame(
|
||||
image_path=...,
|
||||
label_path=...,
|
||||
output_dir=...,
|
||||
save_types=['combined'] # 只保存组合图
|
||||
)
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **默认行为改变**:`visualize_filtered_train.py`默认只保存combined,与之前的"保存所有类型"不同
|
||||
2. **性能提升**:只生成需要的可视化,不会生成后再删除
|
||||
3. **依赖关系**:combined类型需要2d、3d、bev都生成,但不单独保存它们
|
||||
4. **磁盘空间**:如果样本很多,建议只保存combined
|
||||
2532
tools/scripts_for_gt/visualization/plots.py
Executable file
2532
tools/scripts_for_gt/visualization/plots.py
Executable file
File diff suppressed because it is too large
Load Diff
849
tools/scripts_for_gt/visualization/plots_3d.py
Executable file
849
tools/scripts_for_gt/visualization/plots_3d.py
Executable file
@@ -0,0 +1,849 @@
|
||||
"""3D visualization plotting utilities.
|
||||
|
||||
Self-contained module extracted from yolov5-3d/utils/plots.py.
|
||||
Contains only the functions required by visualize_single_frame.py and
|
||||
visualize_batch.py for ground-truth 3D visualization, with no cross-repo
|
||||
dependencies.
|
||||
"""
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import torch
|
||||
from ultralytics.utils.plotting import Annotator # noqa: F401 (re-exported)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Color helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class Colors:
|
||||
"""Provides an RGB color palette derived from Ultralytics color scheme."""
|
||||
|
||||
def __init__(self):
|
||||
hexs = (
|
||||
"FF3838", "FF9D97", "FF701F", "FFB21D", "CFD231",
|
||||
"48F90A", "92CC17", "3DDB86", "1A9334", "00D4BB",
|
||||
"2C99A8", "00C2FF", "344593", "6473FF", "0018EC",
|
||||
"8438FF", "520085", "CB38FF", "FF95C8", "FF37C7",
|
||||
)
|
||||
self.palette = [self.hex2rgb(f"#{c}") for c in hexs]
|
||||
self.n = len(self.palette)
|
||||
|
||||
def __call__(self, i, bgr=False):
|
||||
"""Return colour for index ``i`` (BGR if ``bgr=True``, else RGB)."""
|
||||
c = self.palette[int(i) % self.n]
|
||||
return (c[2], c[1], c[0]) if bgr else c
|
||||
|
||||
@staticmethod
|
||||
def hex2rgb(h):
|
||||
"""Convert hex colour string to (R, G, B) tuple."""
|
||||
return tuple(int(h[1 + i: 1 + i + 2], 16) for i in (0, 2, 4))
|
||||
|
||||
|
||||
colors = Colors() # module-level singleton; callers: from plots_3d import colors
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3D geometry helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def rotation_3d_in_axis(points, angles, axis=0):
|
||||
"""Rotate *points* around a specified camera-frame axis.
|
||||
|
||||
Args:
|
||||
points (np.ndarray): (N, 3) array of 3D points.
|
||||
angles (float): Rotation angle in radians.
|
||||
axis (int): 0=X, 1=Y, 2=Z.
|
||||
|
||||
Returns:
|
||||
np.ndarray: Rotated points (N, 3).
|
||||
"""
|
||||
rot_sin = np.sin(angles)
|
||||
rot_cos = np.cos(angles)
|
||||
ones = np.ones_like(rot_cos)
|
||||
zeros = np.zeros_like(rot_cos)
|
||||
|
||||
if axis == 1:
|
||||
rot_mat = np.stack([
|
||||
np.stack([rot_cos, zeros, -rot_sin]),
|
||||
np.stack([zeros, ones, zeros]),
|
||||
np.stack([rot_sin, zeros, rot_cos]),
|
||||
])
|
||||
elif axis == 2:
|
||||
rot_mat = np.stack([
|
||||
np.stack([rot_cos, rot_sin, zeros]),
|
||||
np.stack([-rot_sin, rot_cos, zeros]),
|
||||
np.stack([zeros, zeros, ones]),
|
||||
])
|
||||
elif axis == 0:
|
||||
rot_mat = np.stack([
|
||||
np.stack([ones, zeros, zeros]),
|
||||
np.stack([zeros, rot_cos, rot_sin]),
|
||||
np.stack([zeros, -rot_sin, rot_cos]),
|
||||
])
|
||||
else:
|
||||
raise ValueError(f"axis must be in {{0, 1, 2}}, got {axis}")
|
||||
|
||||
return np.dot(points, rot_mat)
|
||||
|
||||
|
||||
def compute_3d_box_corners_4face(center_3d, dimensions, rotation, face_type=0):
|
||||
"""Compute the 8 corners of a 3D bounding box from a face-center point.
|
||||
|
||||
Args:
|
||||
center_3d (array-like): (x, y, z) centre of the specified face in camera coords.
|
||||
dimensions (array-like): (length, height, width) of the box in metres.
|
||||
rotation (float): rot_y — rotation about the Y axis in radians.
|
||||
face_type (int): 0=front, 1=tail/rear, 2=left, 3=right, -1=box centre.
|
||||
|
||||
Returns:
|
||||
np.ndarray: (8, 3) corner coordinates in camera frame.
|
||||
"""
|
||||
l, h, w = dimensions
|
||||
|
||||
corners_norm = np.stack(np.unravel_index(np.arange(8), [2] * 3), axis=1).astype(np.float64)
|
||||
corners_norm = corners_norm[[0, 1, 3, 2, 4, 5, 7, 6]]
|
||||
|
||||
offsets = {
|
||||
1: [0, 0.5, 0.5], # tail
|
||||
0: [1, 0.5, 0.5], # front
|
||||
3: [0.5, 0.5, 0], # right
|
||||
2: [0.5, 0.5, 1], # left
|
||||
-1: [0.5, 0.5, 0.5], # whole centre
|
||||
}
|
||||
corners_norm = corners_norm - offsets.get(face_type, offsets[-1])
|
||||
|
||||
corners = np.array([l, h, w]).reshape(1, 3) * corners_norm.reshape(8, 3)
|
||||
corners = rotation_3d_in_axis(corners, rotation, axis=1)
|
||||
corners += np.array(center_3d).reshape(1, 3)
|
||||
return corners
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fisheye (KB) distortion helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def apply_fisheye_distortion(x, y, distort_coeffs):
|
||||
"""Apply Kannala-Brandt fisheye distortion to normalised camera coordinates.
|
||||
|
||||
Args:
|
||||
x (float): Normalised x coordinate (x3d / z3d).
|
||||
y (float): Normalised y coordinate (y3d / z3d).
|
||||
distort_coeffs (list): [k1, k2, k3, k4] KB coefficients.
|
||||
|
||||
Returns:
|
||||
tuple[float, float]: Distorted normalised coordinates (xd, yd).
|
||||
"""
|
||||
if not distort_coeffs or len(distort_coeffs) < 4:
|
||||
return x, y
|
||||
|
||||
k1, k2, k3, k4 = distort_coeffs[:4]
|
||||
r = np.sqrt(x * x + y * y)
|
||||
if r < 1e-8:
|
||||
return x, y
|
||||
|
||||
theta = np.arctan(r)
|
||||
theta_d = theta * (1 + k1 * theta**2 + k2 * theta**4 + k3 * theta**6 + k4 * theta**8)
|
||||
scale = theta_d / r
|
||||
return x * scale, y * scale
|
||||
|
||||
|
||||
def remove_fisheye_distortion(xd, yd, distort_coeffs, max_iter=20):
|
||||
"""Remove KB fisheye distortion from normalised camera coordinates.
|
||||
|
||||
Args:
|
||||
xd (float): Distorted normalised x coordinate.
|
||||
yd (float): Distorted normalised y coordinate.
|
||||
distort_coeffs (list): [k1, k2, k3, k4] KB coefficients.
|
||||
max_iter (int): Maximum Newton-Raphson iterations.
|
||||
|
||||
Returns:
|
||||
tuple[float, float]: Undistorted normalised coordinates (xn, yn).
|
||||
"""
|
||||
if not distort_coeffs or len(distort_coeffs) < 4:
|
||||
return xd, yd
|
||||
|
||||
k1, k2, k3, k4 = distort_coeffs[:4]
|
||||
r_d = np.sqrt(xd * xd + yd * yd)
|
||||
if r_d < 1e-8:
|
||||
return xd, yd
|
||||
|
||||
theta_d = r_d
|
||||
theta = theta_d / (1 + k1 * theta_d * theta_d)
|
||||
|
||||
for _ in range(max_iter):
|
||||
theta2 = theta * theta
|
||||
theta4 = theta2 * theta2
|
||||
theta6 = theta4 * theta2
|
||||
theta8 = theta4 * theta4
|
||||
|
||||
f = theta * (1 + k1 * theta2 + k2 * theta4 + k3 * theta6 + k4 * theta8) - theta_d
|
||||
f_prime = 1 + 3 * k1 * theta2 + 5 * k2 * theta4 + 7 * k3 * theta6 + 9 * k4 * theta8
|
||||
|
||||
theta_new = theta - f / f_prime
|
||||
if abs(theta_new - theta) < 1e-8:
|
||||
theta = theta_new
|
||||
break
|
||||
theta = theta_new
|
||||
|
||||
r = np.tan(theta)
|
||||
scale = r / r_d
|
||||
return xd * scale, yd * scale
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3D-to-2D projection helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def project_3d_to_2d_with_distortion(points_3d, calib):
|
||||
"""Project 3D points to 2D using KB fisheye camera calibration.
|
||||
|
||||
Args:
|
||||
points_3d (np.ndarray): (N, 3) points in camera coordinates.
|
||||
calib (dict): Camera parameters ``fx``, ``fy``, ``cx``, ``cy``,
|
||||
and optional ``distort_coeffs``.
|
||||
|
||||
Returns:
|
||||
np.ndarray: (N, 2) image coordinates (NaN for behind-camera points).
|
||||
"""
|
||||
fx, fy = calib['fx'], calib['fy']
|
||||
cx, cy = calib['cx'], calib['cy']
|
||||
distort_coeffs = calib.get('distort_coeffs', [])
|
||||
|
||||
points_2d = []
|
||||
for x, y, z in points_3d:
|
||||
if z > 0.1:
|
||||
xn, yn = x / z, y / z
|
||||
xd, yd = apply_fisheye_distortion(xn, yn, distort_coeffs)
|
||||
points_2d.append([fx * xd + cx, fy * yd + cy])
|
||||
else:
|
||||
points_2d.append([np.nan, np.nan])
|
||||
|
||||
return np.array(points_2d)
|
||||
|
||||
|
||||
def sample_3d_edge(p1, p2, num_samples=10):
|
||||
"""Uniformly sample *num_samples* points along the 3D edge from *p1* to *p2*.
|
||||
|
||||
Args:
|
||||
p1 (array-like): Start point (x, y, z).
|
||||
p2 (array-like): End point (x, y, z).
|
||||
num_samples (int): Number of sample points.
|
||||
|
||||
Returns:
|
||||
np.ndarray: (num_samples, 3) sampled 3D points.
|
||||
"""
|
||||
t = np.linspace(0, 1, num_samples).reshape(-1, 1)
|
||||
return p1 + t * (p2 - p1)
|
||||
|
||||
|
||||
def project_3d_box_edges_with_distortion(corners_3d, calib, samples_per_edge=10):
|
||||
"""Project 3D box edges to 2D by sampling, handling fisheye distortion.
|
||||
|
||||
Args:
|
||||
corners_3d (np.ndarray): (8, 3) 3D corner coordinates.
|
||||
calib (dict): Camera calibration dict.
|
||||
samples_per_edge (int): Number of samples per edge.
|
||||
|
||||
Returns:
|
||||
dict: Mapping edge_name → (N, 2) 2D projected points.
|
||||
"""
|
||||
edges = {
|
||||
'back_0': (4, 5), 'back_1': (5, 6), 'back_2': (6, 7), 'back_3': (7, 4),
|
||||
'connect_0': (0, 4), 'connect_1': (1, 5), 'connect_2': (2, 6), 'connect_3': (3, 7),
|
||||
'front_0': (0, 1), 'front_1': (1, 2), 'front_2': (2, 3), 'front_3': (3, 0),
|
||||
'front_x1': (0, 2), 'front_x2': (1, 3),
|
||||
}
|
||||
|
||||
return {
|
||||
name: project_3d_to_2d_with_distortion(
|
||||
sample_3d_edge(corners_3d[i], corners_3d[j], samples_per_edge), calib
|
||||
)
|
||||
for name, (i, j) in edges.items()
|
||||
}
|
||||
|
||||
|
||||
def plot_box3d_on_img_with_distortion(img, edge_points_2d,
|
||||
color_front=(255, 0, 0),
|
||||
color_back=(0, 0, 255),
|
||||
color_side=(0, 255, 255),
|
||||
thickness=1):
|
||||
"""Draw a 3D box on *img* using pre-projected edge point lists (fisheye-aware).
|
||||
|
||||
Args:
|
||||
img (np.ndarray): BGR image to draw on.
|
||||
edge_points_2d (dict): Output of :func:`project_3d_box_edges_with_distortion`.
|
||||
color_front (tuple): BGR colour for front-face edges.
|
||||
color_back (tuple): BGR colour for back-face edges.
|
||||
color_side (tuple): BGR colour for side connecting edges.
|
||||
thickness (int): Line thickness in pixels.
|
||||
|
||||
Returns:
|
||||
np.ndarray: Modified image.
|
||||
"""
|
||||
front_edges = {'front_0', 'front_1', 'front_2', 'front_3', 'front_x1', 'front_x2'}
|
||||
back_edges = {'back_0', 'back_1', 'back_2', 'back_3'}
|
||||
|
||||
for edge_name, points in edge_points_2d.items():
|
||||
if np.any(np.isnan(points)):
|
||||
continue
|
||||
pts = points.astype(np.int32)
|
||||
if edge_name in front_edges:
|
||||
color = color_front
|
||||
elif edge_name in back_edges:
|
||||
color = color_back
|
||||
else:
|
||||
color = color_side
|
||||
cv2.polylines(img, [pts], isClosed=False, color=color, thickness=thickness, lineType=cv2.LINE_AA)
|
||||
|
||||
return img
|
||||
|
||||
|
||||
def project_3d_to_2d_with_calib(points_3d, calib):
|
||||
"""Project 3D points to 2D using pinhole calibration (no distortion).
|
||||
|
||||
Args:
|
||||
points_3d (np.ndarray): (N, 3) points in camera coordinates.
|
||||
calib (dict): Camera parameters ``fx``, ``fy``, ``cx``, ``cy``.
|
||||
|
||||
Returns:
|
||||
np.ndarray: (N, 2) image coordinates (NaN for behind-camera points).
|
||||
"""
|
||||
fx, fy = calib['fx'], calib['fy']
|
||||
cx, cy = calib['cx'], calib['cy']
|
||||
|
||||
points_2d = []
|
||||
for x, y, z in points_3d:
|
||||
if z > 0.1:
|
||||
points_2d.append([fx * x / z + cx, fy * y / z + cy])
|
||||
else:
|
||||
points_2d.append([np.nan, np.nan])
|
||||
|
||||
return np.array(points_2d)
|
||||
|
||||
|
||||
def project_3d_to_2d_simple(points_3d, img_size):
|
||||
"""Project 3D points to 2D using a simple estimated pinhole model.
|
||||
|
||||
Args:
|
||||
points_3d (np.ndarray): (N, 3) points in camera coordinates.
|
||||
img_size (tuple[int, int]): ``(width, height)`` of the image.
|
||||
|
||||
Returns:
|
||||
np.ndarray: (N, 2) image coordinates.
|
||||
"""
|
||||
w, h = img_size
|
||||
fx = fy = w * 1.2
|
||||
cx, cy = w / 2, h / 2
|
||||
|
||||
points_2d = []
|
||||
for x, y, z in points_3d:
|
||||
if z > 0.1:
|
||||
points_2d.append([fx * x / z + cx, fy * y / z + cy])
|
||||
else:
|
||||
points_2d.append([np.nan, np.nan])
|
||||
|
||||
return np.array(points_2d)
|
||||
|
||||
|
||||
def plot_box3d_on_img(img, corners_2d,
|
||||
color_front=(255, 0, 0),
|
||||
color_back=(0, 0, 255),
|
||||
color_side=(0, 255, 255),
|
||||
thickness=1):
|
||||
"""Draw a 3D bounding box on *img* from 2D projected corners.
|
||||
|
||||
Args:
|
||||
img (np.ndarray): BGR image to draw on.
|
||||
corners_2d (np.ndarray): (8, 2) projected corner coordinates.
|
||||
color_front (tuple): BGR colour for front-face edges (indices 0-3).
|
||||
color_back (tuple): BGR colour for back-face edges (indices 4-7).
|
||||
color_side (tuple): BGR colour for connecting side edges.
|
||||
thickness (int): Line thickness in pixels.
|
||||
|
||||
Returns:
|
||||
np.ndarray: Modified image.
|
||||
"""
|
||||
line_indices = (
|
||||
(4, 5), (5, 6), (6, 7), (7, 4), # back face
|
||||
(0, 4), (1, 5), (2, 6), (3, 7), # side edges
|
||||
(0, 1), (1, 2), (2, 3), (3, 0), (0, 2), (1, 3), # front face + X mark
|
||||
)
|
||||
front_edges = {(0, 1), (1, 2), (2, 3), (3, 0), (0, 2), (1, 3)}
|
||||
back_edges = {(4, 5), (5, 6), (6, 7), (7, 4)}
|
||||
corners = corners_2d.astype(np.int32)
|
||||
|
||||
for start, end in line_indices:
|
||||
try:
|
||||
pt1 = (corners[start, 0], corners[start, 1])
|
||||
pt2 = (corners[end, 0], corners[end, 1])
|
||||
if (start, end) in front_edges:
|
||||
cv2.line(img, pt1, pt2, color_front, thickness, cv2.LINE_AA)
|
||||
elif (start, end) in back_edges:
|
||||
cv2.line(img, pt1, pt2, color_back, thickness, cv2.LINE_AA)
|
||||
else:
|
||||
cv2.line(img, pt1, pt2, color_side, thickness, cv2.LINE_AA)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return img
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3D box reconstruction from target label format
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _reconstruct_3d_box_from_face(face_uv, face_z, dims, rot_y, face_type, calib):
|
||||
"""Reconstruct 3D box corners from a visible face centre.
|
||||
|
||||
Args:
|
||||
face_uv (tuple[float, float]): Pixel coordinates (u, v) of the face centre.
|
||||
face_z (float): Depth of the face centre in metres.
|
||||
dims (array-like): (length, height, width) in metres.
|
||||
rot_y (float): Yaw rotation in radians.
|
||||
face_type (int): 0=front, 1=rear, 2=left, 3=right.
|
||||
calib (dict): Camera calibration dict.
|
||||
|
||||
Returns:
|
||||
tuple[np.ndarray, list] | None: ``(corners_3d, object_3d)`` or ``None`` on failure.
|
||||
"""
|
||||
if calib is None:
|
||||
return None
|
||||
|
||||
fx, fy = calib['fx'], calib['fy']
|
||||
cx, cy = calib['cx'], calib['cy']
|
||||
distort_coeffs = calib.get('distort_coeffs', [])
|
||||
|
||||
u_face, v_face = face_uv
|
||||
xd = (u_face - cx) / fx
|
||||
yd = (v_face - cy) / fy
|
||||
|
||||
if distort_coeffs and len(distort_coeffs) >= 4:
|
||||
xn, yn = remove_fisheye_distortion(xd, yd, distort_coeffs)
|
||||
else:
|
||||
xn, yn = xd, yd
|
||||
|
||||
l, h, w = dims
|
||||
if np.isnan(l) or np.isnan(h) or np.isnan(w) or np.isnan(rot_y):
|
||||
return None
|
||||
|
||||
face_center_3d = np.array([xn * face_z, yn * face_z, face_z])
|
||||
corners_3d = compute_3d_box_corners_4face(face_center_3d, dims, rot_y, face_type=face_type)
|
||||
object_3d = [face_center_3d[0], face_center_3d[1], face_center_3d[2], l, h, w, rot_y, face_type]
|
||||
return corners_3d, object_3d
|
||||
|
||||
|
||||
def decode_and_reconstruct_3d_box_from_target(target, calib, img_width, img_height,
|
||||
face_3d_classes=None, complete_3d_classes=None):
|
||||
"""Decode a ground-truth target vector and reconstruct its 3D box.
|
||||
|
||||
The target array follows the 48-column format used in ``YOLOGround3DDataset``:
|
||||
|
||||
* col 0 — image index
|
||||
* col 1 — class id
|
||||
* cols 2-5 — normalised 2D bbox [x, y, w, h]
|
||||
* cols 6-8 — 3D centre [x3d, y3d, z3d] in camera coords
|
||||
* cols 9-11 — dimensions [l, h, w]
|
||||
* col 12 — rot_y
|
||||
* cols 13-14 — normalised UV projection of 3D centre
|
||||
* col 15 — alpha
|
||||
* cols 16-23 — front face [x3d, y3d, z3d, alpha, xc, yc, score, visible]
|
||||
* cols 24-31 — rear face (same layout)
|
||||
* cols 32-39 — left face
|
||||
* cols 40-47 — right face
|
||||
|
||||
Args:
|
||||
target (np.ndarray): (48,) label vector.
|
||||
calib (dict): Camera calibration dict; must contain ``depth_scale``.
|
||||
img_width (int): Image width in pixels.
|
||||
img_height (int): Image height in pixels.
|
||||
face_3d_classes (list | None): Class IDs using face-based reconstruction.
|
||||
Defaults to ``[0, 13]`` (vehicles, tricycles).
|
||||
complete_3d_classes (list | None): Class IDs using complete-box
|
||||
reconstruction. Defaults to ``[1, 2, 3]`` (pedestrians, bicycles,
|
||||
riders).
|
||||
|
||||
Returns:
|
||||
dict | None: Result dict with keys ``should_draw``, ``cls``,
|
||||
``corners_3d``, ``face_center_2d``, ``face_color``, ``center_2d``,
|
||||
``object_3d``; or ``None`` if the target is invalid / unsupported.
|
||||
"""
|
||||
if face_3d_classes is None:
|
||||
face_3d_classes = [0, 1, 2, 3, 4, 5, 6, 7, 8] # vehicles
|
||||
if complete_3d_classes is None:
|
||||
complete_3d_classes = [9, 10, 11, 12] # pedestrian / cyclists
|
||||
|
||||
if len(target) < 15 or np.isnan(target[1]):
|
||||
return None
|
||||
|
||||
cls = int(target[1])
|
||||
if cls not in face_3d_classes and cls not in complete_3d_classes:
|
||||
return None
|
||||
|
||||
depth_scale = calib['depth_scale']
|
||||
|
||||
result = {
|
||||
'should_draw': True,
|
||||
'cls': cls,
|
||||
'corners_3d': None,
|
||||
'face_center_2d': None,
|
||||
'face_color': None,
|
||||
'center_2d': None,
|
||||
}
|
||||
|
||||
if cls in face_3d_classes:
|
||||
if len(target) < 48:
|
||||
return None
|
||||
|
||||
face_offsets = [16, 24, 32, 40]
|
||||
# BGR: front=red, rear=blue, left=green, right=yellow
|
||||
face_colors = [(0, 0, 255), (255, 0, 0), (0, 255, 0), (0, 255, 255)]
|
||||
|
||||
best_face_type = -1
|
||||
best_score = -1.0
|
||||
best_face_data = None
|
||||
|
||||
for face_type, face_offset in enumerate(face_offsets):
|
||||
if target.shape[0] < face_offset + 8:
|
||||
continue
|
||||
face_data = target[face_offset:face_offset + 8]
|
||||
_, _, z3d_face, _, xc_face, yc_face, score, is_visible = face_data
|
||||
|
||||
if is_visible == -1 or np.isnan(is_visible) or is_visible != 1:
|
||||
continue
|
||||
if np.isnan(score) or score < 0.3:
|
||||
continue
|
||||
if np.isnan(xc_face) or np.isnan(yc_face) or np.isnan(z3d_face) or z3d_face <= 0:
|
||||
continue
|
||||
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_face_type = face_type
|
||||
best_face_data = face_data
|
||||
|
||||
if best_face_type != -1 and best_face_data is not None:
|
||||
xc_face = best_face_data[4]
|
||||
yc_face = best_face_data[5]
|
||||
z3d_face = best_face_data[2] * depth_scale
|
||||
|
||||
u_face = xc_face * img_width
|
||||
v_face = yc_face * img_height
|
||||
dims = target[9:12]
|
||||
rot_y = target[12]
|
||||
|
||||
result_face = _reconstruct_3d_box_from_face(
|
||||
(u_face, v_face), z3d_face, dims, rot_y, best_face_type, calib
|
||||
)
|
||||
if result_face is not None:
|
||||
corners_3d, object_3d = result_face
|
||||
result['corners_3d'] = corners_3d
|
||||
result['face_center_2d'] = (u_face, v_face)
|
||||
result['face_color'] = face_colors[best_face_type]
|
||||
result['object_3d'] = object_3d
|
||||
|
||||
elif cls in complete_3d_classes:
|
||||
x3d, y3d, z3d = target[6:9]
|
||||
dimensions = target[9:12]
|
||||
rot_y = target[12]
|
||||
xc_norm, yc_norm = target[13:15]
|
||||
|
||||
z3d = z3d * depth_scale
|
||||
|
||||
if np.isnan(z3d) or z3d <= 0 or np.any(np.isnan(dimensions)):
|
||||
return None
|
||||
if np.isnan(x3d) or np.isnan(y3d):
|
||||
return None
|
||||
|
||||
corners_3d = compute_3d_box_corners_4face(
|
||||
np.array([x3d, y3d, z3d]), dimensions, rot_y, face_type=-1
|
||||
)
|
||||
result['corners_3d'] = corners_3d
|
||||
result['center_2d'] = (xc_norm * img_width, yc_norm * img_height)
|
||||
result['object_3d'] = [
|
||||
x3d, y3d, z3d,
|
||||
dimensions[0], dimensions[1], dimensions[2],
|
||||
rot_y, -1,
|
||||
]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Drawing helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def draw_3d_box_from_corners(im, corners_3d, calib, img_shape,
|
||||
face_center_2d=None, face_color=None, thickness=1):
|
||||
"""Project and draw a 3D box given raw corner coordinates.
|
||||
|
||||
Args:
|
||||
im (np.ndarray): BGR image array (H, W, 3).
|
||||
corners_3d (np.ndarray): (8, 3) corners in camera frame
|
||||
(output of :func:`compute_3d_box_corners_4face`).
|
||||
calib (dict | None): Camera calibration dict.
|
||||
img_shape (tuple[int, int]): ``(width, height)`` of *im*.
|
||||
face_center_2d (tuple[float, float] | None): Pixel coords to mark with a dot.
|
||||
face_color (tuple | None): BGR colour for the face-centre dot.
|
||||
thickness (int): Line thickness in pixels.
|
||||
|
||||
Returns:
|
||||
np.ndarray: Modified image.
|
||||
"""
|
||||
w, h = img_shape
|
||||
|
||||
# compute_3d_box_corners_4face places rear at indices 0-3 and front at 4-7.
|
||||
# plot_box3d_on_img expects front at indices 0-3 (drawn red) and rear at 4-7.
|
||||
corners_3d = corners_3d[[4, 5, 6, 7, 0, 1, 2, 3]]
|
||||
|
||||
color_front = (0, 0, 255) # Red (BGR)
|
||||
color_back = (255, 0, 0) # Blue (BGR)
|
||||
color_side = (255, 255, 0) # Cyan (BGR)
|
||||
|
||||
if calib and calib.get('distort_coeffs'):
|
||||
edge_pts = project_3d_box_edges_with_distortion(corners_3d, calib, samples_per_edge=15)
|
||||
im = plot_box3d_on_img_with_distortion(
|
||||
im, edge_pts, color_front, color_back, color_side, thickness=thickness
|
||||
)
|
||||
else:
|
||||
corners_2d = (project_3d_to_2d_with_calib(corners_3d, calib)
|
||||
if calib is not None
|
||||
else project_3d_to_2d_simple(corners_3d, (w, h)))
|
||||
|
||||
if not np.any(np.isnan(corners_2d)):
|
||||
im = plot_box3d_on_img(im, corners_2d, color_front, color_back, color_side, thickness=thickness)
|
||||
|
||||
if face_center_2d is not None and face_color is not None:
|
||||
cv2.circle(im, (int(face_center_2d[0]), int(face_center_2d[1])), 2, face_color, -1, cv2.LINE_AA)
|
||||
|
||||
return im
|
||||
|
||||
|
||||
def plot_3d_boxes_from_decoded_targets(im, decoded_results, paths, calib=None, names=None,
|
||||
label_text=None, scale_factor=2):
|
||||
"""Render 3D boxes from pre-decoded ground-truth targets onto *im*.
|
||||
|
||||
Args:
|
||||
im (torch.Tensor): Batch tensor (N, 3, H, W) normalised [0, 1] in BGR
|
||||
format (as returned by the 3D dataloader).
|
||||
decoded_results (list[list[dict]]): ``decoded_results[i]`` holds a list of
|
||||
result dicts for image *i*, each produced by
|
||||
:func:`decode_and_reconstruct_3d_box_from_target`.
|
||||
paths (list[str]): Image file paths (informational only).
|
||||
calib (dict | list[dict] | None): Camera calibration dict (or first element
|
||||
of a list) used for 3D→2D projection.
|
||||
names (dict | None): Mapping from class id to class name (unused here but
|
||||
kept for API symmetry).
|
||||
label_text (str | None): Text to overlay in the top-left corner, e.g.
|
||||
``"3D GT"``.
|
||||
scale_factor (int): Upscale factor applied to the output image.
|
||||
|
||||
Returns:
|
||||
np.ndarray | None: (H*scale_factor, W*scale_factor, 3) RGB image with 3D
|
||||
boxes drawn, or ``None`` if the input is empty.
|
||||
"""
|
||||
if im.ndim == 3:
|
||||
im = im.unsqueeze(0)
|
||||
|
||||
im_np = im[0].cpu().numpy().transpose(1, 2, 0)
|
||||
im_np = np.ascontiguousarray(im_np * 255, dtype=np.uint8)
|
||||
h_orig, w_orig = im_np.shape[:2]
|
||||
|
||||
h_new = h_orig * scale_factor
|
||||
w_new = w_orig * scale_factor
|
||||
im_bgr = cv2.resize(im_np, (w_new, h_new), interpolation=cv2.INTER_LINEAR)
|
||||
|
||||
if isinstance(calib, list):
|
||||
calib = calib[0] if calib else None
|
||||
|
||||
calib_scaled = None
|
||||
if calib is not None:
|
||||
calib_scaled = {
|
||||
'fx': calib['fx'] * scale_factor,
|
||||
'fy': calib['fy'] * scale_factor,
|
||||
'cx': calib['cx'] * scale_factor,
|
||||
'cy': calib['cy'] * scale_factor,
|
||||
'distort_coeffs': calib.get('distort_coeffs', []),
|
||||
}
|
||||
|
||||
for decoded in decoded_results[0]:
|
||||
if not decoded or not decoded['should_draw'] or decoded['corners_3d'] is None:
|
||||
continue
|
||||
|
||||
cls = decoded['cls']
|
||||
|
||||
if cls in range(9): # vehicles (car/suv/pickup/...special_vehicle/unknown) — face-based
|
||||
face_center_2d = decoded.get('face_center_2d')
|
||||
if face_center_2d is not None:
|
||||
face_center_2d = (
|
||||
face_center_2d[0] * scale_factor,
|
||||
face_center_2d[1] * scale_factor,
|
||||
)
|
||||
im_bgr = draw_3d_box_from_corners(
|
||||
im_bgr,
|
||||
decoded['corners_3d'],
|
||||
calib_scaled,
|
||||
(w_new, h_new),
|
||||
face_center_2d=face_center_2d,
|
||||
face_color=decoded.get('face_color'),
|
||||
)
|
||||
|
||||
elif cls in (9, 10, 11, 12): # pedestrian / bicyclists / bicycles / tricycles — complete box
|
||||
im_bgr = draw_3d_box_from_corners(
|
||||
im_bgr,
|
||||
decoded['corners_3d'],
|
||||
calib_scaled,
|
||||
(w_new, h_new),
|
||||
thickness=2,
|
||||
)
|
||||
if decoded.get('center_2d'):
|
||||
color = colors(cls)
|
||||
u, v = decoded['center_2d']
|
||||
u_s, v_s = int(u * scale_factor), int(v * scale_factor)
|
||||
cv2.circle(im_bgr, (u_s, v_s), 4, color, -1, cv2.LINE_AA)
|
||||
cv2.circle(im_bgr, (u_s, v_s), 6, (255, 255, 255), 2, cv2.LINE_AA)
|
||||
|
||||
if label_text:
|
||||
cv2.putText(im_bgr, label_text, (10, 50), cv2.FONT_HERSHEY_SIMPLEX,
|
||||
1.5, (0, 255, 255), 3, cv2.LINE_AA)
|
||||
|
||||
return cv2.cvtColor(im_bgr, cv2.COLOR_BGR2RGB)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bird's-Eye View (BEV) visualization
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def draw_bev_blank(max_range=200):
|
||||
"""Create a blank BEV image with distance and lateral grid lines.
|
||||
|
||||
Args:
|
||||
max_range (int): Maximum forward range in metres (default 200).
|
||||
|
||||
Returns:
|
||||
np.ndarray: (H, W, 3) BGR image with grid and ego-vehicle box drawn.
|
||||
"""
|
||||
pixels_per_meter = 20
|
||||
img_height = max_range * pixels_per_meter # e.g. 4000 px for 200 m
|
||||
img_width = 100 * pixels_per_meter # lateral range -50 m … +50 m
|
||||
|
||||
bevimg = np.ones((img_height, img_width, 3), dtype=np.uint8) * 255
|
||||
ego_center_x = img_width // 2
|
||||
ego_center_y = img_height # ego position at bottom-centre
|
||||
|
||||
ego_half_l = int(4.5 * pixels_per_meter) // 2
|
||||
ego_half_w = int(1.8 * pixels_per_meter) // 2
|
||||
ego_box = np.array([
|
||||
[ego_center_x - ego_half_w, ego_center_y - ego_half_l],
|
||||
[ego_center_x + ego_half_w, ego_center_y - ego_half_l],
|
||||
[ego_center_x + ego_half_w, ego_center_y + ego_half_l],
|
||||
[ego_center_x - ego_half_w, ego_center_y + ego_half_l],
|
||||
], dtype=np.int32)
|
||||
|
||||
# Horizontal (range) grid
|
||||
grid_step_px = 20 * pixels_per_meter # every 20 m
|
||||
for i in range(max_range // 20 + 1):
|
||||
y_pos = ego_center_y - i * grid_step_px
|
||||
if y_pos >= 0:
|
||||
cv2.line(bevimg, (0, y_pos), (img_width, y_pos), (180, 180, 180), 3, cv2.LINE_AA)
|
||||
cv2.putText(bevimg, f"{i * 20}m", (ego_center_x + 15, y_pos - 15),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 1.2, (80, 80, 80), 2, cv2.LINE_AA)
|
||||
|
||||
# Vertical (lateral) grid
|
||||
lat_step_px = 10 * pixels_per_meter # every 10 m
|
||||
for i in range(-5, 6):
|
||||
x_pos = ego_center_x + i * lat_step_px
|
||||
if 0 <= x_pos < img_width:
|
||||
cv2.line(bevimg, (x_pos, 0), (x_pos, img_height), (180, 180, 180), 3, cv2.LINE_AA)
|
||||
if i != 0:
|
||||
cv2.putText(bevimg, f"{i * 10}m", (x_pos - 40, img_height - 20),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 1.0, (80, 80, 80), 2, cv2.LINE_AA)
|
||||
|
||||
cv2.drawContours(bevimg, [ego_box], 0, (255, 0, 0), -1) # blue-filled ego box
|
||||
return bevimg
|
||||
|
||||
|
||||
def cam_corners_front_rear(pred3d, facetype):
|
||||
"""Compute 8 box corners from a face-centre 3D representation.
|
||||
|
||||
Args:
|
||||
pred3d (array-like): ``[x, y, z, l, h, w, rot_y]`` (7 values).
|
||||
facetype (int | str): Face type identifier — ``0``/``'front'``,
|
||||
``1``/``'tail'``, ``2``/``'left'``, ``3``/``'right'``,
|
||||
``-1``/``'whole'``.
|
||||
|
||||
Returns:
|
||||
np.ndarray: (8, 3) 3D corner coordinates in camera frame.
|
||||
"""
|
||||
dims = pred3d[3:6]
|
||||
corners_norm = np.stack(np.unravel_index(np.arange(8), [2] * 3), axis=1)
|
||||
corners_norm = corners_norm[[0, 1, 3, 2, 4, 5, 7, 6]].astype(float)
|
||||
|
||||
if facetype in ('tail', 1):
|
||||
corners_norm -= [0, 0.5, 0.5]
|
||||
elif facetype in ('front', 0):
|
||||
corners_norm -= [1, 0.5, 0.5]
|
||||
elif facetype in ('right', 3):
|
||||
corners_norm -= [0.5, 0.5, 0]
|
||||
elif facetype in ('left', 2):
|
||||
corners_norm -= [0.5, 0.5, 1]
|
||||
elif facetype in ('whole', -1):
|
||||
corners_norm -= [0.5, 0.5, 0.5]
|
||||
else:
|
||||
raise AssertionError(f"Non-valid face type: {facetype}")
|
||||
|
||||
corners = dims.reshape(1, 3) * corners_norm.reshape(8, 3)
|
||||
corners = rotation_3d_in_axis(corners, pred3d[6], axis=1)
|
||||
corners += pred3d[:3].reshape(1, 3)
|
||||
return corners
|
||||
|
||||
|
||||
def drawbev(bevimg, vehicle3d, is_pred=True):
|
||||
"""Draw a single vehicle box on a BEV image.
|
||||
|
||||
Args:
|
||||
bevimg (np.ndarray): BEV image produced by :func:`draw_bev_blank`.
|
||||
vehicle3d (list | np.ndarray): ``[x, y, z, l, h, w, ..., rot_y, face_type]``.
|
||||
The second-to-last element is ``rot_y`` and the last is ``face_type``.
|
||||
is_pred (bool): ``True`` → red box (prediction); ``False`` → green box (GT).
|
||||
|
||||
Returns:
|
||||
np.ndarray: Modified BEV image.
|
||||
"""
|
||||
x, y, z = vehicle3d[0], vehicle3d[1], vehicle3d[2]
|
||||
l, h, w = vehicle3d[3], vehicle3d[4], vehicle3d[5]
|
||||
rotation_y = vehicle3d[-2]
|
||||
face_type = vehicle3d[-1]
|
||||
|
||||
pixels_per_meter = 20
|
||||
max_range = 200
|
||||
lateral_range = 50
|
||||
|
||||
img_height = bevimg.shape[0]
|
||||
img_width = bevimg.shape[1]
|
||||
ego_center_x = img_width // 2
|
||||
ego_center_y = img_height
|
||||
|
||||
if x > lateral_range or x < -lateral_range or z > max_range or z < 0:
|
||||
return bevimg
|
||||
|
||||
corners = cam_corners_front_rear(np.array([x, y, z, l, h, w, rotation_y]), face_type)
|
||||
xyz3d_front = np.mean(corners[4:8, :], axis=0)
|
||||
xyz3d_center = np.mean(corners[0:8, :], axis=0)
|
||||
|
||||
center = (
|
||||
int(ego_center_x + xyz3d_center[0] * pixels_per_meter),
|
||||
int(ego_center_y - xyz3d_center[2] * pixels_per_meter),
|
||||
)
|
||||
front_point = (
|
||||
int(ego_center_x + xyz3d_front[0] * pixels_per_meter),
|
||||
int(ego_center_y - xyz3d_front[2] * pixels_per_meter),
|
||||
)
|
||||
|
||||
rect = (center, (l * pixels_per_meter, w * pixels_per_meter), np.degrees(rotation_y))
|
||||
box = np.intp(cv2.boxPoints(rect))
|
||||
|
||||
color = (0, 0, 255) if is_pred else (0, 255, 0)
|
||||
cv2.drawContours(bevimg, [box], 0, color, 3, cv2.LINE_AA)
|
||||
cv2.arrowedLine(bevimg, center, front_point, color, thickness=3, tipLength=0.3, line_type=cv2.LINE_AA)
|
||||
|
||||
return bevimg
|
||||
187
tools/scripts_for_gt/visualization/test_visualization.py
Executable file
187
tools/scripts_for_gt/visualization/test_visualization.py
Executable file
@@ -0,0 +1,187 @@
|
||||
"""
|
||||
快速测试脚本 - 自动查找并可视化第一个有效样本
|
||||
|
||||
用途:自动从验证集中查找一个带标签的样本并进行可视化,用于测试脚本功能
|
||||
|
||||
使用方法:
|
||||
python scripts_for_gt/test_visualization.py --data data/mono3d.yaml
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
|
||||
# Add project root to path
|
||||
FILE = Path(__file__).resolve()
|
||||
ROOT = FILE.parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.append(str(ROOT))
|
||||
|
||||
from visualize_single_frame import visualize_single_frame
|
||||
|
||||
|
||||
def find_first_valid_sample(data_yaml_path):
|
||||
"""从数据集配置中查找第一个有效样本
|
||||
|
||||
Args:
|
||||
data_yaml_path: 数据集YAML配置文件路径
|
||||
|
||||
Returns:
|
||||
tuple: (image_path, label_path, calib_path) 或 None
|
||||
"""
|
||||
# 读取数据集配置
|
||||
with open(data_yaml_path, 'r') as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
# 获取验证集路径
|
||||
val_path = data.get('val')
|
||||
if val_path is None:
|
||||
print("错误:数据集配置中未找到'val'字段")
|
||||
return None
|
||||
|
||||
# 如果是相对路径,相对于yaml文件的位置
|
||||
val_path = Path(val_path)
|
||||
if not val_path.is_absolute():
|
||||
val_path = Path(data_yaml_path).parent / val_path
|
||||
|
||||
# 读取验证集图像列表
|
||||
if val_path.is_file():
|
||||
# 如果是文件列表
|
||||
with open(val_path, 'r') as f:
|
||||
image_files = [line.strip() for line in f.readlines() if line.strip()]
|
||||
else:
|
||||
# 如果是目录
|
||||
image_files = list(val_path.glob("**/*.jpg"))
|
||||
image_files.extend(list(val_path.glob("**/*.png")))
|
||||
|
||||
print(f"在验证集中找到 {len(image_files)} 个图像文件")
|
||||
|
||||
# 遍历查找第一个有标签的样本
|
||||
for image_path in image_files:
|
||||
image_rel_path = Path(image_path)
|
||||
|
||||
image_path = val_path.parent / image_rel_path if not image_rel_path.is_absolute() else image_rel_path
|
||||
# 推断标签路径
|
||||
label_path = Path(str(image_path).replace('/images/', '/labels/').replace('.jpg', '.txt').replace('.png', '.txt'))
|
||||
|
||||
# 推断标定路径
|
||||
calib_path = image_path.parent.parent / 'calib' / 'L2_calib' / 'camera4.json'
|
||||
|
||||
if label_path.exists():
|
||||
print(f"\n找到有效样本:")
|
||||
print(f" 图像: {image_path}")
|
||||
print(f" 标签: {label_path}")
|
||||
print(f" 标定: {calib_path if calib_path.exists() else '未找到'}")
|
||||
return str(image_path), str(label_path), str(calib_path) if calib_path.exists() else None
|
||||
|
||||
print("错误:未找到有效样本")
|
||||
return None
|
||||
|
||||
|
||||
def test_visualization(data_yaml_path, output_dir="./test_gt_viz", use_roi=True):
|
||||
"""测试可视化功能
|
||||
|
||||
Args:
|
||||
data_yaml_path: 数据集YAML配置文件路径
|
||||
output_dir: 输出目录
|
||||
use_roi: 是否使用ROI变换
|
||||
"""
|
||||
# 读取数据集配置
|
||||
with open(data_yaml_path, 'r') as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
# 查找第一个有效样本
|
||||
result = find_first_valid_sample(data_yaml_path)
|
||||
if result is None:
|
||||
return
|
||||
|
||||
image_path, label_path, calib_path = result
|
||||
|
||||
# 获取配置参数
|
||||
roi = data.get('roi') # [width, height]
|
||||
virtual_fx = data.get('virtual_fx')
|
||||
ori_img_size = data.get('ori_img_size') # [width, height]
|
||||
|
||||
print(f"\n数据集配置:")
|
||||
print(f" ROI: {roi}")
|
||||
print(f" 虚拟焦距: {virtual_fx}")
|
||||
print(f" 原始图像尺寸: {ori_img_size}")
|
||||
|
||||
# 类别名称
|
||||
names = {
|
||||
0: "vehicle",
|
||||
1: "pedestrian",
|
||||
2: "bicycle",
|
||||
3: "rider",
|
||||
13: "tricycle",
|
||||
}
|
||||
|
||||
print(f"\n开始可视化...")
|
||||
|
||||
try:
|
||||
# 测试1:不使用ROI
|
||||
print("\n=== 测试1:原始图像可视化(不使用ROI)===")
|
||||
visualize_single_frame(
|
||||
image_path=image_path,
|
||||
label_path=label_path,
|
||||
output_dir=f"{output_dir}/no_roi",
|
||||
calib_path=calib_path,
|
||||
roi_size=None,
|
||||
virtual_fx=None,
|
||||
ori_img_size=None,
|
||||
names=names,
|
||||
)
|
||||
print("✓ 测试1完成")
|
||||
|
||||
# 测试2:使用ROI(如果配置了)
|
||||
if use_roi and roi is not None and virtual_fx is not None:
|
||||
print("\n=== 测试2:ROI变换后可视化 ===")
|
||||
visualize_single_frame(
|
||||
image_path=image_path,
|
||||
label_path=label_path,
|
||||
output_dir=f"{output_dir}/with_roi",
|
||||
calib_path=calib_path,
|
||||
roi_size=tuple(roi),
|
||||
virtual_fx=virtual_fx,
|
||||
ori_img_size=tuple(ori_img_size) if ori_img_size else None,
|
||||
names=names,
|
||||
)
|
||||
print("✓ 测试2完成")
|
||||
|
||||
print(f"\n✅ 所有测试完成!结果保存在: {output_dir}")
|
||||
print("\n生成的文件:")
|
||||
output_path = Path(output_dir)
|
||||
for file in sorted(output_path.rglob("*.jpg")):
|
||||
print(f" {file.relative_to(output_path)}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 测试失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description="快速测试真值可视化功能")
|
||||
parser.add_argument("--data", type=str, default="data/mono3d.yaml",
|
||||
help="数据集YAML配置文件")
|
||||
parser.add_argument("--output", type=str, default="./test_gt_viz",
|
||||
help="输出目录")
|
||||
parser.add_argument("--no-roi", action="store_true",
|
||||
help="不测试ROI变换")
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
|
||||
test_visualization(
|
||||
data_yaml_path=args.data,
|
||||
output_dir=args.output,
|
||||
use_roi=not args.no_roi,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
209
tools/scripts_for_gt/visualization/visualize_batch.py
Executable file
209
tools/scripts_for_gt/visualization/visualize_batch.py
Executable file
@@ -0,0 +1,209 @@
|
||||
"""
|
||||
批量可视化脚本
|
||||
|
||||
用途:批量读取多帧图像和对应的真值标签文件,生成可视化结果
|
||||
|
||||
使用方法:
|
||||
python scripts_for_gt/visualize_batch.py \
|
||||
--image-dir /path/to/images \
|
||||
--label-dir /path/to/labels \
|
||||
--output output_dir \
|
||||
--roi 704 352 \
|
||||
--virtual-fx 500 \
|
||||
--max-samples 100
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
# Add project root to path
|
||||
FILE = Path(__file__).resolve()
|
||||
ROOT = FILE.parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.append(str(ROOT))
|
||||
|
||||
from visualize_single_frame import (
|
||||
DEFAULT_CLASS_NAMES,
|
||||
format_target_classes,
|
||||
resolve_target_class_ids,
|
||||
visualize_single_frame,
|
||||
)
|
||||
|
||||
|
||||
def resolve_label_path(label_dir, image_stem, preferred_extension=None):
|
||||
"""按同名文件自动查找标签,支持 .txt / .json。"""
|
||||
candidate_extensions = []
|
||||
if preferred_extension:
|
||||
normalized_extension = preferred_extension if preferred_extension.startswith(".") else f".{preferred_extension}"
|
||||
candidate_extensions.append(normalized_extension)
|
||||
|
||||
for extension in (".txt", ".json"):
|
||||
if extension not in candidate_extensions:
|
||||
candidate_extensions.append(extension)
|
||||
|
||||
for extension in candidate_extensions:
|
||||
candidate_path = label_dir / f"{image_stem}{extension}"
|
||||
if candidate_path.exists():
|
||||
return candidate_path
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def visualize_batch(
|
||||
image_dir,
|
||||
label_dir,
|
||||
output_dir,
|
||||
calib_dir=None,
|
||||
roi_size=None,
|
||||
virtual_fx=None,
|
||||
ori_img_size=None,
|
||||
max_samples=None,
|
||||
sample_interval=1,
|
||||
file_extension=".jpg",
|
||||
label_extension=None,
|
||||
target_class_ids=None,
|
||||
):
|
||||
"""批量可视化图像
|
||||
|
||||
Args:
|
||||
image_dir: 图像目录
|
||||
label_dir: 标签目录
|
||||
output_dir: 输出目录
|
||||
calib_dir: 标定文件目录(包含L2_calib/camera4.json)
|
||||
roi_size: ROI尺寸 (width, height)
|
||||
virtual_fx: 虚拟焦距
|
||||
ori_img_size: 原始图像尺寸 (width, height)
|
||||
max_samples: 最大处理样本数
|
||||
sample_interval: 采样间隔,每隔 N 帧处理一帧
|
||||
file_extension: 图像文件扩展名
|
||||
label_extension: 标签文件扩展名,默认自动在 .txt/.json 中查找
|
||||
target_class_ids: 目标类别ID集合,None表示不过滤
|
||||
"""
|
||||
image_dir = Path(image_dir)
|
||||
label_dir = Path(label_dir)
|
||||
output_dir = Path(output_dir)
|
||||
|
||||
# 获取所有图像文件
|
||||
image_files = sorted(image_dir.glob(f"*{file_extension}"))
|
||||
|
||||
if sample_interval > 1:
|
||||
image_files = image_files[::sample_interval]
|
||||
|
||||
if max_samples is not None:
|
||||
image_files = image_files[:max_samples]
|
||||
|
||||
print(f"找到 {len(image_files)} 个图像文件(采样间隔={sample_interval})")
|
||||
|
||||
# 类别名称
|
||||
names = DEFAULT_CLASS_NAMES.copy()
|
||||
|
||||
if target_class_ids is not None:
|
||||
print(f"类别过滤: {format_target_classes(target_class_ids, names=names)}")
|
||||
|
||||
success_count = 0
|
||||
error_count = 0
|
||||
|
||||
for i, image_path in enumerate(image_files):
|
||||
print(f"\n[{i+1}/{len(image_files)}] 处理: {image_path.name}")
|
||||
|
||||
# 查找对应的标签文件
|
||||
label_path = resolve_label_path(label_dir, image_path.stem, preferred_extension=label_extension)
|
||||
if label_path is None:
|
||||
expected_extensions = [label_extension] if label_extension else [".txt", ".json"]
|
||||
print(f" 警告:未找到标签文件 {image_path.stem},已检查扩展名: {expected_extensions}")
|
||||
error_count += 1
|
||||
continue
|
||||
|
||||
# 查找对应的标定文件
|
||||
if calib_dir is not None:
|
||||
# 兼容 L2_calib 子目录和直接放在 calib/ 下两种结构
|
||||
calib_path = Path(calib_dir) / "L2_calib" / "camera4.json"
|
||||
if not calib_path.exists():
|
||||
calib_path = Path(calib_dir) / "camera4.json"
|
||||
else:
|
||||
# 自动推断:依次尝试两种常见目录结构
|
||||
base = image_path.parent.parent
|
||||
calib_path = base / "calib" / "L2_calib" / "camera4.json"
|
||||
if not calib_path.exists():
|
||||
calib_path = base / "calib" / "camera4.json"
|
||||
|
||||
if not calib_path.exists():
|
||||
calib_path = None
|
||||
print(f" 警告:未找到标定文件,将仅进行2D可视化")
|
||||
|
||||
try:
|
||||
# 可视化单帧
|
||||
sample_output_dir = output_dir
|
||||
visualize_single_frame(
|
||||
image_path=str(image_path),
|
||||
label_path=str(label_path),
|
||||
output_dir=str(sample_output_dir),
|
||||
calib_path=str(calib_path) if calib_path else None,
|
||||
roi_size=roi_size,
|
||||
virtual_fx=virtual_fx,
|
||||
ori_img_size=ori_img_size,
|
||||
names=names,
|
||||
save_types='combined', # 只保存合成图
|
||||
target_class_ids=target_class_ids,
|
||||
)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
print(f" 错误:{e}")
|
||||
error_count += 1
|
||||
|
||||
print(f"\n处理完成!")
|
||||
print(f" 成功: {success_count}")
|
||||
print(f" 失败: {error_count}")
|
||||
print(f" 输出目录: {output_dir}")
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description="批量可视化真值标签")
|
||||
parser.add_argument("--image-dir", type=str, required=True, help="图像目录")
|
||||
parser.add_argument("--label-dir", type=str, required=True, help="标签目录")
|
||||
parser.add_argument("--output", type=str, default="./gt_visualization_batch", help="输出目录")
|
||||
parser.add_argument("--calib-dir", type=str, default=None,
|
||||
help="标定文件目录(包含L2_calib/camera4.json)")
|
||||
parser.add_argument("--roi", type=int, nargs=2, default=None, metavar=("WIDTH", "HEIGHT"),
|
||||
help="ROI尺寸(宽 高),如: --roi 704 352")
|
||||
parser.add_argument("--virtual-fx", type=float, default=None,
|
||||
help="虚拟焦距,用于深度归一化")
|
||||
parser.add_argument("--ori-img-size", type=int, nargs=2, default=None, metavar=("WIDTH", "HEIGHT"),
|
||||
help="原始图像尺寸(宽 高)")
|
||||
parser.add_argument("--max-samples", type=int, default=None,
|
||||
help="最大处理样本数")
|
||||
parser.add_argument("--sample-interval", type=int, default=1,
|
||||
help="采样间隔,每隔 N 帧处理一帧(默认1,即处理全部)")
|
||||
parser.add_argument("--file-extension", type=str, default=".jpg",
|
||||
help="图像文件扩展名")
|
||||
parser.add_argument("--label-extension", type=str, default=None,
|
||||
help="标签文件扩展名,默认自动在 .txt/.json 中查找")
|
||||
parser.add_argument("--classes", type=str, nargs="+", default=None,
|
||||
help="仅可视化指定类别,支持类别名/类别ID,可传多个值或逗号分隔")
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
target_class_ids = resolve_target_class_ids(args.classes, names=DEFAULT_CLASS_NAMES)
|
||||
|
||||
visualize_batch(
|
||||
image_dir=args.image_dir,
|
||||
label_dir=args.label_dir,
|
||||
output_dir=args.output,
|
||||
calib_dir=args.calib_dir,
|
||||
roi_size=tuple(args.roi) if args.roi else None,
|
||||
virtual_fx=args.virtual_fx,
|
||||
ori_img_size=tuple(args.ori_img_size) if args.ori_img_size else None,
|
||||
max_samples=args.max_samples,
|
||||
sample_interval=args.sample_interval,
|
||||
file_extension=args.file_extension,
|
||||
label_extension=args.label_extension,
|
||||
target_class_ids=target_class_ids,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
54
tools/scripts_for_gt/visualization/visualize_batch.sh
Executable file
54
tools/scripts_for_gt/visualization/visualize_batch.sh
Executable file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
# 对 DL_KPI_SCENE 下的所有 case 批量进行 GT 可视化
|
||||
# 用法:
|
||||
# bash tools/scripts_for_gt/visualization/visualize_batch.sh [SAMPLE_INTERVAL]
|
||||
# 例:
|
||||
# bash tools/scripts_for_gt/visualization/visualize_batch.sh 5 # 每隔5帧处理一帧
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCENE_ROOT="/data1/dongying/Mono3d/G1Q3/dataset_for_evaluation/DL_KPI_SCENE"
|
||||
SAMPLE_INTERVAL="${1:-20}" # 第一个位置参数为采样间隔,默认为20
|
||||
FILE_EXTENSION=".png"
|
||||
|
||||
# 收集所有 case 目录(含 images/ 子目录的)
|
||||
mapfile -t CASE_DIRS < <(find "$SCENE_ROOT" -mindepth 1 -maxdepth 1 -type d | sort)
|
||||
|
||||
echo "共找到 ${#CASE_DIRS[@]} 个 case,采样间隔=${SAMPLE_INTERVAL}"
|
||||
echo "======================================================"
|
||||
|
||||
SUCCESS=0
|
||||
SKIP=0
|
||||
FAIL=0
|
||||
|
||||
for CASE_DIR in "${CASE_DIRS[@]}"; do
|
||||
IMAGE_DIR="${CASE_DIR}/images"
|
||||
LABEL_DIR="${CASE_DIR}/labels_json"
|
||||
OUTPUT_DIR="${CASE_DIR}/vis_json"
|
||||
|
||||
if [[ ! -d "$IMAGE_DIR" ]]; then
|
||||
echo "[SKIP] ${CASE_DIR##*/} — 无 images/ 目录"
|
||||
(( SKIP++ )) || true
|
||||
continue
|
||||
fi
|
||||
if [[ ! -d "$LABEL_DIR" ]]; then
|
||||
echo "[SKIP] ${CASE_DIR##*/} — 无 labels_json/ 目录"
|
||||
(( SKIP++ )) || true
|
||||
continue
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo ">>> 处理 case: ${CASE_DIR##*/}"
|
||||
python tools/scripts_for_gt/visualization/visualize_batch.py \
|
||||
--image-dir "$IMAGE_DIR" \
|
||||
--label-dir "$LABEL_DIR" \
|
||||
--output "$OUTPUT_DIR" \
|
||||
--file-extension "$FILE_EXTENSION" \
|
||||
--sample-interval "$SAMPLE_INTERVAL" \
|
||||
&& (( SUCCESS++ )) || (( FAIL++ )) || true
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "======================================================"
|
||||
echo "全部完成:成功=${SUCCESS} 跳过=${SKIP} 失败=${FAIL}"
|
||||
|
||||
60
tools/scripts_for_gt/visualization/visualize_batch_case.sh
Executable file
60
tools/scripts_for_gt/visualization/visualize_batch_case.sh
Executable file
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env bash
|
||||
# 对单个 case 目录批量进行 GT 可视化
|
||||
# 用法:
|
||||
# bash tools/scripts_for_gt/visualization/visualize_batch_case.sh [SAMPLE_INTERVAL] [CLASSES]
|
||||
# 例:
|
||||
# bash tools/scripts_for_gt/visualization/visualize_batch_case.sh
|
||||
# bash tools/scripts_for_gt/visualization/visualize_batch_case.sh 5 car
|
||||
# bash tools/scripts_for_gt/visualization/visualize_batch_case.sh 1 car,pedestrian
|
||||
# OUTPUT_DIR=/tmp/vis_car bash tools/scripts_for_gt/visualization/visualize_batch_case.sh 1 car
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
IMAGE_DIR="/data1/dongying/Mono3d/G1Q3/dataset_for_evaluation/DL_KPI_SCENE/019b6aa1-a8ed-7029-8b8c-68311394dade/images"
|
||||
LABEL_DIR="/data1/dongying/Mono3d/G1Q3/dataset_for_evaluation/DL_KPI_SCENE/019b6aa1-a8ed-7029-8b8c-68311394dade/labels_json"
|
||||
DEFAULT_OUTPUT_DIR="/data1/dongying/Mono3d/G1Q3/dataset_for_evaluation/DL_KPI_SCENE/019b6aa1-a8ed-7029-8b8c-68311394dade/vis_json_car_suv"
|
||||
FILE_EXTENSION=".png"
|
||||
SAMPLE_INTERVAL="${1:-1}"
|
||||
TARGET_CLASSES="${2:-${TARGET_CLASSES:-car,suv}}"
|
||||
|
||||
sanitize_for_path() {
|
||||
local value="$1"
|
||||
value="${value//,/__}"
|
||||
value="${value// /}"
|
||||
value="${value//\//_}"
|
||||
value="${value//:/_}"
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
if [[ -n "${OUTPUT_DIR:-}" ]]; then
|
||||
FINAL_OUTPUT_DIR="${OUTPUT_DIR}"
|
||||
elif [[ -n "${TARGET_CLASSES}" ]]; then
|
||||
FINAL_OUTPUT_DIR="${DEFAULT_OUTPUT_DIR}_$(sanitize_for_path "${TARGET_CLASSES}")"
|
||||
else
|
||||
FINAL_OUTPUT_DIR="${DEFAULT_OUTPUT_DIR}"
|
||||
fi
|
||||
|
||||
echo "image_dir: ${IMAGE_DIR}"
|
||||
echo "label_dir: ${LABEL_DIR}"
|
||||
echo "output_dir: ${FINAL_OUTPUT_DIR}"
|
||||
echo "sample_interval: ${SAMPLE_INTERVAL}"
|
||||
if [[ -n "${TARGET_CLASSES}" ]]; then
|
||||
echo "target_classes: ${TARGET_CLASSES}"
|
||||
else
|
||||
echo "target_classes: all"
|
||||
fi
|
||||
|
||||
PYTHON_ARGS=(
|
||||
python tools/scripts_for_gt/visualization/visualize_batch.py
|
||||
--image-dir "${IMAGE_DIR}"
|
||||
--label-dir "${LABEL_DIR}"
|
||||
--output "${FINAL_OUTPUT_DIR}"
|
||||
--file-extension "${FILE_EXTENSION}"
|
||||
--sample-interval "${SAMPLE_INTERVAL}"
|
||||
)
|
||||
|
||||
if [[ -n "${TARGET_CLASSES}" ]]; then
|
||||
PYTHON_ARGS+=(--classes "${TARGET_CLASSES}")
|
||||
fi
|
||||
|
||||
"${PYTHON_ARGS[@]}"
|
||||
474
tools/scripts_for_gt/visualization/visualize_filtered_train.py
Executable file
474
tools/scripts_for_gt/visualization/visualize_filtered_train.py
Executable file
@@ -0,0 +1,474 @@
|
||||
"""
|
||||
训练数据过滤可视化脚本
|
||||
|
||||
用途:从训练集中筛选符合条件的样本并进行可视化
|
||||
|
||||
功能:
|
||||
- 支持类别过滤(如:只可视化包含两轮车的样本)
|
||||
- 支持空间范围过滤(如:只可视化指定3D空间范围内的目标)
|
||||
- 支持批量处理
|
||||
|
||||
使用方法:
|
||||
# 可视化包含两轮车的样本(两轮车在左右10米、纵向50米范围内)
|
||||
python scripts_for_gt/visualize_filtered_train.py \
|
||||
--data data/mono3d.yaml \
|
||||
--target-class bicycle \
|
||||
--x-range -10 10 \
|
||||
--z-range 0 50 \
|
||||
--max-samples 20
|
||||
|
||||
# 可视化包含行人的样本
|
||||
python scripts_for_gt/visualize_filtered_train.py \
|
||||
--data data/mono3d.yaml \
|
||||
--target-class pedestrian \
|
||||
--max-samples 50
|
||||
|
||||
# 可视化特定空间范围内的所有车辆
|
||||
python scripts_for_gt/visualize_filtered_train.py \
|
||||
--data data/mono3d.yaml \
|
||||
--target-class vehicle \
|
||||
--x-range -5 5 \
|
||||
--z-range 10 30 \
|
||||
--max-samples 30
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
import numpy as np
|
||||
|
||||
# Add project root to path
|
||||
FILE = Path(__file__).resolve()
|
||||
ROOT = FILE.parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.append(str(ROOT))
|
||||
|
||||
from visualize_single_frame import (
|
||||
visualize_single_frame,
|
||||
load_label_file,
|
||||
)
|
||||
|
||||
|
||||
# 类别名称到ID的映射
|
||||
CLASS_NAME_TO_ID = {
|
||||
'vehicle': 0,
|
||||
'pedestrian': 1,
|
||||
'bicycle': 2,
|
||||
'rider': 3,
|
||||
'tricycle': 13,
|
||||
}
|
||||
|
||||
CLASS_ID_TO_NAME = {v: k for k, v in CLASS_NAME_TO_ID.items()}
|
||||
|
||||
|
||||
def check_sample_matches_filter(
|
||||
label_path,
|
||||
target_class_id=None,
|
||||
x_range=None,
|
||||
y_range=None,
|
||||
z_range=None,
|
||||
min_targets=1,
|
||||
):
|
||||
"""检查样本是否符合过滤条件
|
||||
|
||||
Args:
|
||||
label_path: 标签文件路径
|
||||
target_class_id: 目标类别ID(None表示不过滤类别)
|
||||
x_range: X坐标范围 (min, max),单位:米,None表示不过滤
|
||||
y_range: Y坐标范围 (min, max),单位:米,None表示不过滤
|
||||
z_range: Z坐标范围 (min, max),单位:米,None表示不过滤
|
||||
min_targets: 至少需要多少个符合条件的目标
|
||||
|
||||
Returns:
|
||||
tuple: (matches, matched_count, matched_objects_info)
|
||||
- matches: bool,是否符合条件
|
||||
- matched_count: int,符合条件的目标数量
|
||||
- matched_objects_info: list of dict,符合条件的目标信息
|
||||
"""
|
||||
try:
|
||||
labels = load_label_file(label_path)
|
||||
except Exception as e:
|
||||
print(f" 警告:无法读取标签文件 {label_path}: {e}")
|
||||
return False, 0, []
|
||||
|
||||
if len(labels) == 0:
|
||||
return False, 0, []
|
||||
|
||||
matched_objects = []
|
||||
|
||||
for label in labels:
|
||||
# 检查类别
|
||||
cls = int(label[0])
|
||||
if target_class_id is not None and cls != target_class_id:
|
||||
continue
|
||||
|
||||
# 检查是否有3D信息
|
||||
x3d, y3d, z3d = label[6:9]
|
||||
if np.isnan(x3d) or np.isnan(y3d) or np.isnan(z3d):
|
||||
continue
|
||||
|
||||
# 检查空间范围
|
||||
in_range = True
|
||||
|
||||
if x_range is not None:
|
||||
if not (x_range[0] <= x3d <= x_range[1]):
|
||||
in_range = False
|
||||
|
||||
if y_range is not None:
|
||||
if not (y_range[0] <= y3d <= y_range[1]):
|
||||
in_range = False
|
||||
|
||||
if z_range is not None:
|
||||
if not (z_range[0] <= z3d <= z_range[1]):
|
||||
in_range = False
|
||||
|
||||
if in_range:
|
||||
obj_info = {
|
||||
'class': cls,
|
||||
'class_name': CLASS_ID_TO_NAME.get(cls, f'class_{cls}'),
|
||||
'x3d': x3d,
|
||||
'y3d': y3d,
|
||||
'z3d': z3d,
|
||||
'bbox_2d': label[1:5],
|
||||
}
|
||||
matched_objects.append(obj_info)
|
||||
|
||||
matched_count = len(matched_objects)
|
||||
matches = matched_count >= min_targets
|
||||
|
||||
return matches, matched_count, matched_objects
|
||||
|
||||
|
||||
def find_filtered_samples(
|
||||
data_yaml_path,
|
||||
target_class=None,
|
||||
x_range=None,
|
||||
y_range=None,
|
||||
z_range=None,
|
||||
min_targets=1,
|
||||
max_samples=None,
|
||||
split='train',
|
||||
):
|
||||
"""从数据集中查找符合过滤条件的样本
|
||||
|
||||
Args:
|
||||
data_yaml_path: 数据集YAML配置文件路径
|
||||
target_class: 目标类别名称(如'bicycle')
|
||||
x_range: X坐标范围 (min, max)
|
||||
y_range: Y坐标范围 (min, max)
|
||||
z_range: Z坐标范围 (min, max)
|
||||
min_targets: 最少目标数量
|
||||
max_samples: 最大返回样本数
|
||||
split: 数据集划分('train'或'val')
|
||||
|
||||
Returns:
|
||||
list: 符合条件的样本信息列表
|
||||
"""
|
||||
# 读取数据集配置
|
||||
with open(data_yaml_path, 'r') as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
# 获取数据集路径
|
||||
dataset_path = data.get(split)
|
||||
if dataset_path is None:
|
||||
print(f"错误:数据集配置中未找到'{split}'字段")
|
||||
return []
|
||||
|
||||
# 如果是相对路径,相对于yaml文件的位置
|
||||
dataset_path = Path(dataset_path)
|
||||
if not dataset_path.is_absolute():
|
||||
dataset_path = Path(data_yaml_path).parent / dataset_path
|
||||
|
||||
# 读取图像列表
|
||||
if dataset_path.is_file():
|
||||
# 如果是文件列表
|
||||
with open(dataset_path, 'r') as f:
|
||||
image_files = [line.strip() for line in f.readlines() if line.strip()]
|
||||
else:
|
||||
# 如果是目录
|
||||
image_files = list(dataset_path.glob("**/*.jpg"))
|
||||
image_files.extend(list(dataset_path.glob("**/*.png")))
|
||||
|
||||
print(f"在{split}集中找到 {len(image_files)} 个图像文件")
|
||||
|
||||
# 转换类别名称到ID
|
||||
target_class_id = None
|
||||
if target_class is not None:
|
||||
if target_class.lower() in CLASS_NAME_TO_ID:
|
||||
target_class_id = CLASS_NAME_TO_ID[target_class.lower()]
|
||||
else:
|
||||
print(f"警告:未知类别 '{target_class}',将不过滤类别")
|
||||
|
||||
# 打印过滤条件
|
||||
print("\n过滤条件:")
|
||||
if target_class_id is not None:
|
||||
print(f" 目标类别: {target_class} (ID={target_class_id})")
|
||||
else:
|
||||
print(f" 目标类别: 所有类别")
|
||||
|
||||
if x_range is not None:
|
||||
print(f" X范围: [{x_range[0]:.1f}, {x_range[1]:.1f}] 米 (左右)")
|
||||
if y_range is not None:
|
||||
print(f" Y范围: [{y_range[0]:.1f}, {y_range[1]:.1f}] 米 (上下)")
|
||||
if z_range is not None:
|
||||
print(f" Z范围: [{z_range[0]:.1f}, {z_range[1]:.1f}] 米 (前后)")
|
||||
print(f" 最少目标数: {min_targets}")
|
||||
|
||||
# 遍历查找符合条件的样本
|
||||
matched_samples = []
|
||||
|
||||
for i, image_file in enumerate(image_files):
|
||||
if (i + 1) % 100 == 0:
|
||||
print(f" 进度: {i+1}/{len(image_files)}, 已找到 {len(matched_samples)} 个符合条件的样本")
|
||||
|
||||
image_rel_path = Path(image_file)
|
||||
image_path = dataset_path.parent / image_rel_path if not image_rel_path.is_absolute() else image_rel_path
|
||||
|
||||
# 推断标签路径
|
||||
label_path = Path(str(image_path).replace('/images/', '/labels/').replace('.jpg', '.txt').replace('.png', '.txt'))
|
||||
|
||||
if not label_path.exists():
|
||||
continue
|
||||
|
||||
# 检查是否符合过滤条件
|
||||
matches, matched_count, matched_objects = check_sample_matches_filter(
|
||||
label_path,
|
||||
target_class_id=target_class_id,
|
||||
x_range=x_range,
|
||||
y_range=y_range,
|
||||
z_range=z_range,
|
||||
min_targets=min_targets,
|
||||
)
|
||||
|
||||
if matches:
|
||||
# 推断标定路径
|
||||
calib_path = image_path.parent.parent / 'calib' / 'L2_calib' / 'camera4.json'
|
||||
|
||||
sample_info = {
|
||||
'image_path': str(image_path),
|
||||
'label_path': str(label_path),
|
||||
'calib_path': str(calib_path) if calib_path.exists() else None,
|
||||
'matched_count': matched_count,
|
||||
'matched_objects': matched_objects,
|
||||
}
|
||||
matched_samples.append(sample_info)
|
||||
|
||||
# 检查是否达到最大样本数
|
||||
if max_samples is not None and len(matched_samples) >= max_samples:
|
||||
print(f"\n已找到 {len(matched_samples)} 个符合条件的样本(达到最大限制)")
|
||||
break
|
||||
|
||||
print(f"\n总共找到 {len(matched_samples)} 个符合条件的样本")
|
||||
|
||||
return matched_samples
|
||||
|
||||
|
||||
def visualize_filtered_samples(
|
||||
data_yaml_path,
|
||||
output_dir="./filtered_train_viz",
|
||||
target_class=None,
|
||||
x_range=None,
|
||||
y_range=None,
|
||||
z_range=None,
|
||||
min_targets=1,
|
||||
max_samples=20,
|
||||
use_roi=False,
|
||||
split='train',
|
||||
save_types=None,
|
||||
):
|
||||
"""可视化符合条件的样本
|
||||
|
||||
Args:
|
||||
data_yaml_path: 数据集YAML配置文件路径
|
||||
output_dir: 输出目录
|
||||
target_class: 目标类别名称
|
||||
x_range: X坐标范围
|
||||
y_range: Y坐标范围
|
||||
z_range: Z坐标范围
|
||||
min_targets: 最少目标数量
|
||||
max_samples: 最大样本数
|
||||
use_roi: 是否使用ROI变换
|
||||
split: 数据集划分
|
||||
save_types: 保存类型列表 ['2d', '3d', 'bev', 'combined'],None表示全部保存
|
||||
"""
|
||||
# 读取数据集配置
|
||||
with open(data_yaml_path, 'r') as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
# 查找符合条件的样本
|
||||
matched_samples = find_filtered_samples(
|
||||
data_yaml_path=data_yaml_path,
|
||||
target_class=target_class,
|
||||
x_range=x_range,
|
||||
y_range=y_range,
|
||||
z_range=z_range,
|
||||
min_targets=min_targets,
|
||||
max_samples=max_samples,
|
||||
split=split,
|
||||
)
|
||||
|
||||
if len(matched_samples) == 0:
|
||||
print("\n未找到符合条件的样本!")
|
||||
return
|
||||
|
||||
# 获取配置参数
|
||||
roi = data.get('roi') if use_roi else None
|
||||
virtual_fx = data.get('virtual_fx') if use_roi else None
|
||||
ori_img_size = data.get('ori_img_size') if use_roi else None
|
||||
|
||||
# 类别名称
|
||||
names = {
|
||||
0: "vehicle",
|
||||
1: "pedestrian",
|
||||
2: "bicycle",
|
||||
3: "rider",
|
||||
4: "motorcycle",
|
||||
13: "tricycle",
|
||||
}
|
||||
|
||||
print(f"\n开始可视化 {len(matched_samples)} 个样本...")
|
||||
|
||||
success_count = 0
|
||||
error_count = 0
|
||||
|
||||
for i, sample in enumerate(matched_samples):
|
||||
print(f"\n[{i+1}/{len(matched_samples)}] 处理样本:")
|
||||
print(f" 图像: {Path(sample['image_path']).name}")
|
||||
print(f" 符合条件的目标数: {sample['matched_count']}")
|
||||
|
||||
# 打印目标信息
|
||||
for j, obj in enumerate(sample['matched_objects']):
|
||||
print(f" 目标{j+1}: {obj['class_name']}, "
|
||||
f"位置=({obj['x3d']:.2f}, {obj['y3d']:.2f}, {obj['z3d']:.2f})m")
|
||||
|
||||
try:
|
||||
# 创建子目录
|
||||
sample_name = Path(sample['image_path']).stem
|
||||
sample_output_dir = Path(output_dir)
|
||||
|
||||
# 可视化
|
||||
visualize_single_frame(
|
||||
image_path=sample['image_path'],
|
||||
label_path=sample['label_path'],
|
||||
output_dir=str(sample_output_dir),
|
||||
calib_path=sample['calib_path'],
|
||||
roi_size=tuple(roi) if roi else None,
|
||||
virtual_fx=virtual_fx,
|
||||
ori_img_size=tuple(ori_img_size) if ori_img_size else None,
|
||||
names=names,
|
||||
save_types=save_types,
|
||||
)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
print(f" 错误: {e}")
|
||||
error_count += 1
|
||||
|
||||
print(f"\n===== 可视化完成 =====")
|
||||
print(f"成功: {success_count}")
|
||||
print(f"失败: {error_count}")
|
||||
print(f"输出目录: {output_dir}")
|
||||
|
||||
# 生成摘要文件
|
||||
summary_path = Path(output_dir) / "summary.txt"
|
||||
with open(summary_path, 'w', encoding='utf-8') as f:
|
||||
f.write("过滤可视化摘要\n")
|
||||
f.write("=" * 50 + "\n\n")
|
||||
|
||||
f.write("过滤条件:\n")
|
||||
if target_class:
|
||||
f.write(f" 目标类别: {target_class}\n")
|
||||
if x_range:
|
||||
f.write(f" X范围: [{x_range[0]:.1f}, {x_range[1]:.1f}] 米\n")
|
||||
if y_range:
|
||||
f.write(f" Y范围: [{y_range[0]:.1f}, {y_range[1]:.1f}] 米\n")
|
||||
if z_range:
|
||||
f.write(f" Z范围: [{z_range[0]:.1f}, {z_range[1]:.1f}] 米\n")
|
||||
f.write(f" 最少目标数: {min_targets}\n")
|
||||
f.write(f" 数据集: {split}\n\n")
|
||||
|
||||
f.write(f"可视化结果:\n")
|
||||
f.write(f" 总样本数: {len(matched_samples)}\n")
|
||||
f.write(f" 成功: {success_count}\n")
|
||||
f.write(f" 失败: {error_count}\n\n")
|
||||
|
||||
f.write("样本列表:\n")
|
||||
f.write("-" * 50 + "\n")
|
||||
for i, sample in enumerate(matched_samples):
|
||||
f.write(f"{i+1}. {Path(sample['image_path']).name}\n")
|
||||
f.write(f" 符合条件的目标数: {sample['matched_count']}\n")
|
||||
for obj in sample['matched_objects']:
|
||||
f.write(f" - {obj['class_name']}: ({obj['x3d']:.2f}, {obj['y3d']:.2f}, {obj['z3d']:.2f})m\n")
|
||||
f.write("\n")
|
||||
|
||||
print(f"摘要已保存: {summary_path}")
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description="训练数据过滤可视化脚本")
|
||||
|
||||
# 基本参数
|
||||
parser.add_argument("--data", type=str, default="data/mono3d.yaml",
|
||||
help="数据集YAML配置文件")
|
||||
parser.add_argument("--output", type=str, default="/data1/dongying/Mono3d/G1M3/filtered_train_viz",
|
||||
help="输出目录")
|
||||
parser.add_argument("--split", type=str, default="train", choices=['train', 'val'],
|
||||
help="数据集划分(train或val)")
|
||||
|
||||
# 过滤条件
|
||||
parser.add_argument("--target-class", type=str, default='bicycle',
|
||||
choices=['vehicle','pedestrian','bicycle'],
|
||||
help="目标类别")
|
||||
parser.add_argument("--x-range", type=float, nargs=2, default=[-10, 10], metavar=("MIN", "MAX"),
|
||||
help="X坐标范围(左右),单位:米,如: --x-range -10 10")
|
||||
parser.add_argument("--y-range", type=float, nargs=2, default=None, metavar=("MIN", "MAX"),
|
||||
help="Y坐标范围(上下),单位:米")
|
||||
parser.add_argument("--z-range", type=float, nargs=2, default=[0, 50], metavar=("MIN", "MAX"),
|
||||
help="Z坐标范围(前后),单位:米,如: --z-range 0 50")
|
||||
parser.add_argument("--min-targets", type=int, default=1,
|
||||
help="最少目标数量")
|
||||
parser.add_argument("--max-samples", type=int, default=5000,
|
||||
help="最大可视化样本数")
|
||||
|
||||
# 可视化选项
|
||||
parser.add_argument("--use-roi", action="store_true",
|
||||
help="使用ROI变换")
|
||||
parser.add_argument("--save-types", type=str, nargs='+',
|
||||
choices=['2d', '3d', 'bev', 'combined'],
|
||||
default=['combined'],
|
||||
help="保存的图像类型(可多选),默认只保存combined")
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
|
||||
# 验证范围参数
|
||||
if args.x_range and args.x_range[0] >= args.x_range[1]:
|
||||
print("错误:X范围的最小值必须小于最大值")
|
||||
return
|
||||
if args.y_range and args.y_range[0] >= args.y_range[1]:
|
||||
print("错误:Y范围的最小值必须小于最大值")
|
||||
return
|
||||
if args.z_range and args.z_range[0] >= args.z_range[1]:
|
||||
print("错误:Z范围的最小值必须小于最大值")
|
||||
return
|
||||
|
||||
visualize_filtered_samples(
|
||||
data_yaml_path=args.data,
|
||||
output_dir=args.output,
|
||||
target_class=args.target_class,
|
||||
x_range=tuple(args.x_range) if args.x_range else None,
|
||||
y_range=tuple(args.y_range) if args.y_range else None,
|
||||
z_range=tuple(args.z_range) if args.z_range else None,
|
||||
min_targets=args.min_targets,
|
||||
max_samples=args.max_samples,
|
||||
use_roi=args.use_roi,
|
||||
split=args.split,
|
||||
save_types=args.save_types,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
846
tools/scripts_for_gt/visualization/visualize_single_frame.py
Executable file
846
tools/scripts_for_gt/visualization/visualize_single_frame.py
Executable file
@@ -0,0 +1,846 @@
|
||||
"""
|
||||
单帧图像真值可视化脚本
|
||||
|
||||
用途:读取一帧图像和对应的真值标签文件,实现2D和3D的可视化
|
||||
|
||||
使用方法:
|
||||
python scripts_for_gt/visualize_single_frame.py \
|
||||
--image path/to/image.jpg \
|
||||
--label path/to/label.txt \
|
||||
--output output_dir \
|
||||
--roi 704 352 \
|
||||
--virtual-fx 500
|
||||
|
||||
示例:
|
||||
python scripts_for_gt/visualize_single_frame.py \
|
||||
--image /path/to/images/frame_001.jpg \
|
||||
--label /path/to/labels/frame_001.txt \
|
||||
--output ./gt_visualization
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
# Add the visualization directory to path so plots_3d can be imported directly
|
||||
FILE = Path(__file__).resolve()
|
||||
VIS_DIR = FILE.parent
|
||||
if str(VIS_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(VIS_DIR))
|
||||
|
||||
from plots_3d import (
|
||||
decode_and_reconstruct_3d_box_from_target,
|
||||
plot_3d_boxes_from_decoded_targets,
|
||||
draw_bev_blank,
|
||||
drawbev,
|
||||
Annotator,
|
||||
colors,
|
||||
)
|
||||
|
||||
JSON_TYPE_NAME_TO_CLASS = {
|
||||
# Vehicles — face_3d_classes [0-8]
|
||||
"vehicle": 0, # generic vehicle fallback -> car
|
||||
"car": 0,
|
||||
"suv": 1,
|
||||
"pickup": 2,
|
||||
"medium_car": 3,
|
||||
"van": 4,
|
||||
"bus": 5,
|
||||
"truck": 6,
|
||||
"tanker": 6,
|
||||
"large_truck": 6,
|
||||
"construction_vehicle": 6,
|
||||
"special_vehicle": 7,
|
||||
"unknown": 8,
|
||||
# Pedestrian / cyclists — complete_3d_classes [9-12]
|
||||
"pedestrian": 9,
|
||||
"bicyclist": 10,
|
||||
"motorcyclist": 10,
|
||||
"bicycle": 11,
|
||||
"motorcycle": 11,
|
||||
"rider": 10, # generic rider fallback -> bicyclist
|
||||
"tricycle": 12,
|
||||
"tricyclist": 12,
|
||||
# Other objects
|
||||
"traffic_sign": 13,
|
||||
"wheel": 14,
|
||||
"plate": 15,
|
||||
"face": 16,
|
||||
}
|
||||
|
||||
DEFAULT_CLASS_NAMES = {
|
||||
0: "car", 1: "suv", 2: "pickup", 3: "medium_car", 4: "van",
|
||||
5: "bus", 6: "truck", 7: "special_vehicle", 8: "unknown",
|
||||
9: "pedestrian", 10: "bicyclist", 11: "bicycle", 12: "tricycle",
|
||||
13: "traffic_sign", 14: "wheel", 15: "plate", 16: "face",
|
||||
}
|
||||
|
||||
CLASS_FILTER_GROUPS = {
|
||||
"vehicle": set(range(0, 9)),
|
||||
"vehicles": set(range(0, 9)),
|
||||
"vru": {9, 10, 11, 12},
|
||||
"cyclist": {10, 11},
|
||||
"cyclists": {10, 11},
|
||||
"two_wheeler": {10, 11},
|
||||
"two_wheelers": {10, 11},
|
||||
}
|
||||
|
||||
|
||||
def _safe_float(value):
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return np.nan
|
||||
|
||||
|
||||
def _map_json_label_class(obj):
|
||||
type_name = str(obj.get("type_name", "")).strip().lower()
|
||||
if type_name in JSON_TYPE_NAME_TO_CLASS:
|
||||
return JSON_TYPE_NAME_TO_CLASS[type_name]
|
||||
|
||||
type_value = obj.get("type", None)
|
||||
try:
|
||||
type_id = int(type_value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
# Legacy coarse ids written directly into JSON (old 5-class scheme).
|
||||
# 0=vehicle->car(0), 1=pedestrian->9, 2=bicycle->11, 3=rider->10, 13=tricycle->12
|
||||
legacy_type_map = {0: 0, 1: 9, 2: 11, 3: 10, 13: 12}
|
||||
if type_name in legacy_type_map:
|
||||
return JSON_TYPE_NAME_TO_CLASS.get(type_name, legacy_type_map.get(type_id))
|
||||
if type_id in legacy_type_map:
|
||||
return legacy_type_map[type_id]
|
||||
|
||||
# Fine-grained ids from convert_json_to_json.py / convert_txt_to_json.py.
|
||||
# 0=car,1=suv,2=pickup,3=medium_car,4=van,5=bus,6=truck,7=special_vehicle,8=unknown
|
||||
if 0 <= type_id <= 8:
|
||||
return type_id
|
||||
if type_id == 9: return 9 # pedestrian
|
||||
if type_id == 10: return 10 # bicyclist / motorcyclist
|
||||
if type_id == 11: return 11 # bicycle / motorcycle
|
||||
if type_id == 12: return 12 # tricycle
|
||||
return None
|
||||
|
||||
|
||||
def _xyxy_to_xywhn(box2d, img_width, img_height):
|
||||
x1, y1, x2, y2 = [_safe_float(v) for v in box2d[:4]]
|
||||
return np.array([
|
||||
(x1 + x2) * 0.5 / img_width,
|
||||
(y1 + y2) * 0.5 / img_height,
|
||||
(x2 - x1) / img_width,
|
||||
(y2 - y1) / img_height,
|
||||
], dtype=np.float32)
|
||||
|
||||
|
||||
def _normalize_uv(x_px, y_px, img_width, img_height):
|
||||
return x_px / img_width, y_px / img_height
|
||||
|
||||
|
||||
def _iter_json_objects(payload):
|
||||
if isinstance(payload, dict):
|
||||
if all(isinstance(value, dict) for value in payload.values()):
|
||||
return payload.values()
|
||||
if isinstance(payload.get("objects"), list):
|
||||
return payload["objects"]
|
||||
return []
|
||||
if isinstance(payload, list):
|
||||
return payload
|
||||
return []
|
||||
|
||||
|
||||
def _load_json_label_file(label_path, image_size=None):
|
||||
"""读取 labels_json 文件并转换为可视化脚本使用的47维标签格式。"""
|
||||
if image_size is None:
|
||||
raise ValueError("JSON 标签需要提供 image_size=(width, height) 以便将 box2d 像素坐标转换为归一化坐标")
|
||||
|
||||
img_width, img_height = image_size
|
||||
if img_width <= 0 or img_height <= 0:
|
||||
raise ValueError(f"无效的 image_size: {image_size}")
|
||||
|
||||
with open(label_path, "r", encoding="utf-8") as f:
|
||||
payload = json.load(f)
|
||||
|
||||
labels = []
|
||||
for obj in _iter_json_objects(payload):
|
||||
cls = _map_json_label_class(obj)
|
||||
if cls is None:
|
||||
continue
|
||||
|
||||
box2d = obj.get("box2d") or []
|
||||
if len(box2d) < 4:
|
||||
continue
|
||||
|
||||
temp = np.full(47, np.nan, dtype=np.float32)
|
||||
temp[0] = cls
|
||||
temp[1:5] = _xyxy_to_xywhn(box2d, img_width, img_height)
|
||||
|
||||
ori_3d = obj.get("3d_ori") or []
|
||||
ori_values = [_safe_float(v) for v in ori_3d]
|
||||
if len(ori_values) >= 12 and ori_values[0] != -1.0:
|
||||
temp[5:12] = np.array(ori_values[:7], dtype=np.float32)
|
||||
temp[12], temp[13] = _normalize_uv(ori_values[7], ori_values[8], img_width, img_height)
|
||||
temp[14] = ori_values[11]
|
||||
|
||||
for face_name, offset in (("3d_front", 15), ("3d_back", 23), ("3d_left", 31), ("3d_right", 39)):
|
||||
face_values = [_safe_float(v) for v in (obj.get(face_name) or [])]
|
||||
if len(face_values) < 8:
|
||||
continue
|
||||
temp[offset:offset + 4] = np.array(face_values[:4], dtype=np.float32)
|
||||
if face_values[0] == -1.0:
|
||||
temp[offset + 4] = -1.0
|
||||
temp[offset + 5] = -1.0
|
||||
else:
|
||||
temp[offset + 4], temp[offset + 5] = _normalize_uv(face_values[4], face_values[5], img_width, img_height)
|
||||
temp[offset + 6] = face_values[6]
|
||||
temp[offset + 7] = face_values[7]
|
||||
|
||||
labels.append(temp)
|
||||
|
||||
if len(labels) > 0:
|
||||
return np.stack(labels, axis=0)
|
||||
return np.empty((0, 47), dtype=np.float32)
|
||||
|
||||
|
||||
def load_label_file(label_path, image_size=None):
|
||||
"""读取标签文件,返回47维的标签数组。
|
||||
|
||||
标签格式参考:
|
||||
- txt: 6/18/50列格式
|
||||
- json: labels_json 目录下的逐帧 JSON 标签
|
||||
|
||||
Returns:
|
||||
np.ndarray: (N, 47) 标签数组
|
||||
"""
|
||||
label_path = Path(label_path)
|
||||
if label_path.suffix.lower() == ".json":
|
||||
return _load_json_label_file(label_path, image_size=image_size)
|
||||
|
||||
labels = []
|
||||
with open(label_path, 'r', encoding='utf-8') as f:
|
||||
lines = f.read().strip().splitlines()
|
||||
for line in lines:
|
||||
line = line.strip().split(' ')
|
||||
line = line[:-1] # 移除最后一列(通常是换行符)
|
||||
|
||||
if len(line) == 6: # 仅2D bbox
|
||||
temp = np.full(47, np.nan, dtype=np.float32)
|
||||
temp[:6] = np.array([float(x) for x in line[:6]], np.float32)
|
||||
labels.append(temp)
|
||||
elif len(line) == 18 and int(float(line[0])) in [1, 2, 3]: # 行人、自行车、骑手
|
||||
temp = np.full(47, np.nan, dtype=np.float32)
|
||||
useful_gt_info = [float(line[x]) for x in (list(range(14)) + [16])]
|
||||
temp[0:15] = np.array(useful_gt_info, np.float32)
|
||||
labels.append(temp)
|
||||
elif len(line) == 50 and int(float(line[0])) in [0, 13]: # 车辆、三轮车(带面信息)
|
||||
used_gt_ = [float(line[x]) for x in (list(range(14)) + [16])]
|
||||
temp = used_gt_
|
||||
temp.extend([float(x) for x in line[18:]])
|
||||
labels.append(np.array(temp, np.float32))
|
||||
else:
|
||||
print(f"警告:跳过无效标签格式,长度={len(line)}")
|
||||
|
||||
if len(labels) > 0:
|
||||
labels = np.stack(labels, axis=0)
|
||||
assert labels.shape[1] == 47, f"标签应为47列,实际为{labels.shape[1]}列"
|
||||
else:
|
||||
labels = np.empty((0, 47), dtype=np.float32)
|
||||
|
||||
return labels
|
||||
|
||||
|
||||
def resolve_target_class_ids(target_classes, names=None):
|
||||
"""解析目标类别,支持类别名、类别ID、逗号分隔和常见类别组。"""
|
||||
if not target_classes:
|
||||
return None
|
||||
|
||||
if isinstance(target_classes, str):
|
||||
target_classes = [target_classes]
|
||||
|
||||
names = DEFAULT_CLASS_NAMES if names is None else names
|
||||
name_to_id = {str(name).strip().lower(): cls for cls, name in names.items()}
|
||||
alias_to_ids = {name: {cls} for name, cls in JSON_TYPE_NAME_TO_CLASS.items()}
|
||||
alias_to_ids.update(CLASS_FILTER_GROUPS)
|
||||
|
||||
resolved_ids = set()
|
||||
unknown_classes = []
|
||||
|
||||
for raw_value in target_classes:
|
||||
for class_token in str(raw_value).split(","):
|
||||
class_name = class_token.strip().lower()
|
||||
if not class_name:
|
||||
continue
|
||||
|
||||
if class_name in name_to_id:
|
||||
resolved_ids.add(name_to_id[class_name])
|
||||
continue
|
||||
|
||||
if class_name in alias_to_ids:
|
||||
resolved_ids.update(alias_to_ids[class_name])
|
||||
continue
|
||||
|
||||
try:
|
||||
resolved_ids.add(int(class_name))
|
||||
except ValueError:
|
||||
unknown_classes.append(class_name)
|
||||
|
||||
if unknown_classes:
|
||||
available_classes = ", ".join(str(name) for name in names.values())
|
||||
raise ValueError(
|
||||
f"未知类别: {', '.join(sorted(set(unknown_classes)))};"
|
||||
f"可用类别: {available_classes}"
|
||||
)
|
||||
|
||||
return resolved_ids or None
|
||||
|
||||
|
||||
def format_target_classes(target_class_ids, names=None):
|
||||
"""将类别ID集合格式化为便于打印的名称列表。"""
|
||||
if not target_class_ids:
|
||||
return "all"
|
||||
|
||||
names = DEFAULT_CLASS_NAMES if names is None else names
|
||||
return ", ".join(names.get(cls_id, str(cls_id)) for cls_id in sorted(target_class_ids))
|
||||
|
||||
|
||||
def filter_labels_by_class(labels, target_class_ids):
|
||||
"""按类别过滤标签。"""
|
||||
if target_class_ids is None or len(labels) == 0:
|
||||
return labels
|
||||
|
||||
valid_mask = ~np.isnan(labels[:, 0])
|
||||
class_mask = np.zeros(len(labels), dtype=bool)
|
||||
class_mask[valid_mask] = np.isin(labels[valid_mask, 0].astype(np.int32), sorted(target_class_ids))
|
||||
return labels[class_mask]
|
||||
|
||||
|
||||
def load_calibration(calib_path):
|
||||
"""读取相机标定文件
|
||||
|
||||
Args:
|
||||
calib_path: camera4.json 路径
|
||||
|
||||
Returns:
|
||||
dict: 标定参数字典
|
||||
"""
|
||||
with open(calib_path, 'r') as f:
|
||||
calib_params = json.load(f)
|
||||
return calib_params
|
||||
|
||||
|
||||
def apply_roi_transform(img, labels, calib_params, roi_size, virtual_fx, ori_img_size):
|
||||
"""应用ROI变换:裁剪图像和调整标签
|
||||
|
||||
Args:
|
||||
img: 原始图像 (H, W, 3) BGR
|
||||
labels: 标签数组 (N, 47)
|
||||
calib_params: 标定参数字典
|
||||
roi_size: ROI 尺寸 (width, height)
|
||||
virtual_fx: 虚拟焦距
|
||||
ori_img_size: 原始图像尺寸 (width, height)
|
||||
|
||||
Returns:
|
||||
img_roi: 裁剪后的图像
|
||||
labels_roi: 调整后的标签
|
||||
calib_roi: ROI 标定参数
|
||||
"""
|
||||
oriW, oriH = ori_img_size
|
||||
roi_w, roi_h = roi_size
|
||||
|
||||
# 获取相机参数
|
||||
fx = calib_params['focal_u']
|
||||
fy = calib_params['focal_v']
|
||||
cx = calib_params['cu']
|
||||
cy = calib_params['cv']
|
||||
c_pitch = calib_params['pitch']
|
||||
|
||||
# 计算消失点和裁剪中心
|
||||
vanish_y = cy - fy * np.tan(c_pitch * np.pi / 180)
|
||||
crop_center_x = oriW // 2
|
||||
crop_center_y = vanish_y
|
||||
|
||||
# 计算ROI边界
|
||||
roi_x1 = int(crop_center_x - roi_w / 2.0)
|
||||
roi_y1 = int(crop_center_y - roi_h / 2.0)
|
||||
roi_x2 = roi_x1 + roi_w
|
||||
roi_y2 = roi_y1 + roi_h
|
||||
|
||||
# 裁剪图像
|
||||
img_roi = img[roi_y1:roi_y2, roi_x1:roi_x2, :]
|
||||
|
||||
# 调整标签
|
||||
labels_roi = post_process_labels_to_roi(
|
||||
labels.copy(), oriW, oriH, roi_x1, roi_y1, roi_x2, roi_y2
|
||||
)
|
||||
|
||||
# 计算缩放比例(如果需要resize到特定大小)
|
||||
# 这里假设不需要额外resize,ROI大小即为目标大小
|
||||
scale_x = 1.0
|
||||
scale_y = 1.0
|
||||
|
||||
# 计算ROI标定参数
|
||||
calib_roi = {
|
||||
'fx': fx * scale_x,
|
||||
'fy': fy * scale_y,
|
||||
'cx': (cx - roi_x1) * scale_x,
|
||||
'cy': (cy - roi_y1) * scale_y,
|
||||
'distort_coeffs': calib_params.get('distort_coeffs', []),
|
||||
'target_fx': virtual_fx,
|
||||
'depth_scale': fx / virtual_fx, # 用于将归一化深度转换回原始尺度
|
||||
}
|
||||
|
||||
# 调整z3d深度值(深度归一化)
|
||||
if len(labels_roi) > 0:
|
||||
scale = virtual_fx / calib_roi['fx']
|
||||
labels_roi = scale_z3d(labels_roi, scale)
|
||||
|
||||
return img_roi, labels_roi, calib_roi
|
||||
|
||||
|
||||
def post_process_labels_to_roi(labels, oriW, oriH, roi_x1, roi_y1, roi_x2, roi_y2):
|
||||
"""将标签从原始图像坐标转换到ROI坐标"""
|
||||
if len(labels) == 0:
|
||||
return labels
|
||||
|
||||
roi_width = roi_x2 - roi_x1
|
||||
roi_height = roi_y2 - roi_y1
|
||||
|
||||
# 将归一化的xywh转换为绝对坐标
|
||||
boxes = xywhn2xyxy(labels[:, 1:5], oriW, oriH, padw=0, padh=0)
|
||||
|
||||
# 转换到ROI相对坐标
|
||||
new_x1 = boxes[:, 0] - roi_x1
|
||||
new_y1 = boxes[:, 1] - roi_y1
|
||||
new_x2 = boxes[:, 2] - roi_x1
|
||||
new_y2 = boxes[:, 3] - roi_y1
|
||||
|
||||
# 检查是否完全在ROI外
|
||||
curr_outside = ((new_x1 < 0) & (new_x2 < 0)) | \
|
||||
((new_x1 >= roi_width) & (new_x2 >= roi_width)) | \
|
||||
((new_y1 < 0) & (new_y2 < 0)) | \
|
||||
((new_y1 >= roi_height) & (new_y2 >= roi_height))
|
||||
|
||||
# 裁剪到ROI边界
|
||||
new_x1 = np.clip(new_x1, 0, roi_width - 1)
|
||||
new_y1 = np.clip(new_y1, 0, roi_height - 1)
|
||||
new_x2 = np.clip(new_x2, 0, roi_width - 1)
|
||||
new_y2 = np.clip(new_y2, 0, roi_height - 1)
|
||||
|
||||
# 更新归一化坐标
|
||||
labels[:, 1] = (new_x1 + new_x2) * 0.5 / roi_width
|
||||
labels[:, 2] = (new_y1 + new_y2) * 0.5 / roi_height
|
||||
labels[:, 3] = (new_x2 - new_x1) / roi_width
|
||||
labels[:, 4] = (new_y2 - new_y1) / roi_height
|
||||
|
||||
# 调整UV坐标(中心点投影)
|
||||
# 列索引: (12,13), (19,20), (27,28), (35,36), (43,44)
|
||||
for xi, yi in [(12, 13), (19, 20), (27, 28), (35, 36), (43, 44)]:
|
||||
mask = ~np.isnan(labels[:, xi]) & ~np.isnan(labels[:, yi])
|
||||
if mask.any():
|
||||
labels[mask, xi] = (labels[mask, xi] * oriW - roi_x1) / roi_width
|
||||
labels[mask, yi] = (labels[mask, yi] * oriH - roi_y1) / roi_height
|
||||
|
||||
# 移除完全在ROI外的框
|
||||
labels = labels[~curr_outside]
|
||||
|
||||
return labels
|
||||
|
||||
|
||||
def xywhn2xyxy(x, w=640, h=640, padw=0, padh=0):
|
||||
"""将归一化的xywh转换为xyxy"""
|
||||
y = np.copy(x)
|
||||
y[:, 0] = w * (x[:, 0] - x[:, 2] / 2) + padw # x1
|
||||
y[:, 1] = h * (x[:, 1] - x[:, 3] / 2) + padh # y1
|
||||
y[:, 2] = w * (x[:, 0] + x[:, 2] / 2) + padw # x2
|
||||
y[:, 3] = h * (x[:, 1] + x[:, 3] / 2) + padh # y2
|
||||
return y
|
||||
|
||||
|
||||
def scale_z3d(labels, scale):
|
||||
"""缩放z3d深度坐标
|
||||
|
||||
只缩放Z坐标,因为:
|
||||
- 投影公式:u = fx * X / Z + cx
|
||||
- 同时缩放fx和Z可以保持投影不变
|
||||
- X和Y不需要缩放,因为它们是从UV和Z恢复的
|
||||
"""
|
||||
if len(labels) == 0 or scale == 1.0:
|
||||
return labels
|
||||
|
||||
labels_scaled = labels.copy()
|
||||
|
||||
# 整体中心z3d(列7)
|
||||
labels_scaled[:, 7] *= scale
|
||||
|
||||
# 前面中心z3d(列17)
|
||||
mask = ~np.isnan(labels_scaled[:, 17])
|
||||
labels_scaled[mask, 17] *= scale
|
||||
|
||||
# 后面中心z3d(列25)
|
||||
mask = ~np.isnan(labels_scaled[:, 25])
|
||||
labels_scaled[mask, 25] *= scale
|
||||
|
||||
# 左面中心z3d(列33)
|
||||
mask = ~np.isnan(labels_scaled[:, 33])
|
||||
labels_scaled[mask, 33] *= scale
|
||||
|
||||
# 右面中心z3d(列41)
|
||||
mask = ~np.isnan(labels_scaled[:, 41])
|
||||
labels_scaled[mask, 41] *= scale
|
||||
|
||||
return labels_scaled
|
||||
|
||||
|
||||
def plot_2d_boxes(img, labels, names=None, label_text=None, scale_factor=2):
|
||||
"""绘制2D边界框
|
||||
|
||||
Args:
|
||||
img: 图像 (H, W, 3) BGR
|
||||
labels: 标签数组 (N, 47)
|
||||
names: 类别名称字典
|
||||
label_text: 标签文字(如"2D GT")
|
||||
scale_factor: 放大倍数
|
||||
|
||||
Returns:
|
||||
img_annotated: 带标注的图像 BGR
|
||||
"""
|
||||
h, w = img.shape[:2]
|
||||
|
||||
# 放大图像以获得更清晰的可视化
|
||||
h_new = h * scale_factor
|
||||
w_new = w * scale_factor
|
||||
img_scaled = cv2.resize(img, (w_new, h_new), interpolation=cv2.INTER_LINEAR)
|
||||
|
||||
# 转换为RGB用于Annotator
|
||||
img_rgb = cv2.cvtColor(img_scaled, cv2.COLOR_BGR2RGB)
|
||||
|
||||
# 创建标注器
|
||||
fs = int((h_new + w_new) * 0.01)
|
||||
annotator = Annotator(img_rgb, line_width=max(2, round(fs / 8)), font_size=fs, pil=True)
|
||||
|
||||
# 绘制边界
|
||||
annotator.rectangle([0, 0, w_new, h_new], None, (255, 255, 255), width=2)
|
||||
|
||||
# 绘制每个框
|
||||
for label in labels:
|
||||
if np.isnan(label[0]):
|
||||
continue
|
||||
|
||||
cls = int(label[0])
|
||||
x_center, y_center, box_w, box_h = label[1:5]
|
||||
|
||||
# 转换为像素坐标
|
||||
x1 = (x_center - box_w / 2) * w_new
|
||||
y1 = (y_center - box_h / 2) * h_new
|
||||
x2 = (x_center + box_w / 2) * w_new
|
||||
y2 = (y_center + box_h / 2) * h_new
|
||||
|
||||
box = [x1, y1, x2, y2]
|
||||
color = colors(cls)
|
||||
|
||||
# 类别名称
|
||||
if names:
|
||||
cls_name = names.get(cls, str(cls))
|
||||
else:
|
||||
cls_name = str(cls)
|
||||
|
||||
annotator.box_label(box, cls_name, color=color)
|
||||
|
||||
# 转换回BGR并添加标签文字
|
||||
result = np.array(annotator.im)
|
||||
result = cv2.cvtColor(result, cv2.COLOR_RGB2BGR)
|
||||
|
||||
if label_text:
|
||||
cv2.putText(result, label_text, (10, 50), cv2.FONT_HERSHEY_SIMPLEX,
|
||||
1.5, (0, 255, 255), 3, cv2.LINE_AA)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def visualize_single_frame(
|
||||
image_path,
|
||||
label_path,
|
||||
output_dir,
|
||||
calib_path=None,
|
||||
roi_size=None,
|
||||
virtual_fx=None,
|
||||
ori_img_size=None,
|
||||
names=None,
|
||||
save_types=None,
|
||||
target_class_ids=None,
|
||||
):
|
||||
"""可视化单帧图像的2D和3D真值
|
||||
|
||||
Args:
|
||||
image_path: 图像路径
|
||||
label_path: 标签路径
|
||||
output_dir: 输出目录
|
||||
calib_path: 相机标定文件路径
|
||||
roi_size: ROI尺寸 (width, height),如果为None则不应用ROI
|
||||
virtual_fx: 虚拟焦距
|
||||
ori_img_size: 原始图像尺寸 (width, height)
|
||||
names: 类别名称字典
|
||||
save_types: 保存类型列表 ['2d', '3d', 'bev', 'combined'],None表示全部保存
|
||||
target_class_ids: 目标类别ID集合,None表示不过滤
|
||||
"""
|
||||
# 创建输出目录
|
||||
output_dir = Path(output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 读取图像
|
||||
img = cv2.imread(str(image_path))
|
||||
if img is None:
|
||||
raise FileNotFoundError(f"无法读取图像: {image_path}")
|
||||
print(f"已读取图像: {image_path}, 尺寸: {img.shape[:2]}")
|
||||
|
||||
# 默认类别名称
|
||||
if names is None:
|
||||
names = DEFAULT_CLASS_NAMES.copy()
|
||||
|
||||
# 读取标签
|
||||
labels = load_label_file(label_path, image_size=(img.shape[1], img.shape[0]))
|
||||
print(f"已读取 {len(labels)} 个标签")
|
||||
|
||||
if target_class_ids is not None:
|
||||
raw_label_count = len(labels)
|
||||
labels = filter_labels_by_class(labels, target_class_ids)
|
||||
print(
|
||||
f"类别过滤后保留 {len(labels)}/{raw_label_count} 个标签: "
|
||||
f"{format_target_classes(target_class_ids, names=names)}"
|
||||
)
|
||||
|
||||
# 如果没有指定ori_img_size,使用图像实际尺寸
|
||||
if ori_img_size is None:
|
||||
ori_img_size = (img.shape[1], img.shape[0])
|
||||
|
||||
# 检查是否需要应用ROI变换
|
||||
apply_roi = roi_size is not None and calib_path is not None and virtual_fx is not None
|
||||
|
||||
if apply_roi:
|
||||
# 读取标定文件
|
||||
calib_params = load_calibration(calib_path)
|
||||
print(f"已读取标定文件: {calib_path}")
|
||||
|
||||
# 应用ROI变换
|
||||
img_processed, labels_processed, calib = apply_roi_transform(
|
||||
img, labels, calib_params, roi_size, virtual_fx, ori_img_size
|
||||
)
|
||||
print(f"应用ROI变换,ROI尺寸: {roi_size}")
|
||||
else:
|
||||
# 不应用ROI,使用原始图像和标签
|
||||
img_processed = img
|
||||
labels_processed = labels
|
||||
|
||||
# 如果提供了calib_path,读取标定参数
|
||||
if calib_path is not None:
|
||||
calib_params = load_calibration(calib_path)
|
||||
calib = {
|
||||
'fx': calib_params['focal_u'],
|
||||
'fy': calib_params['focal_v'],
|
||||
'cx': calib_params['cu'],
|
||||
'cy': calib_params['cv'],
|
||||
'distort_coeffs': calib_params.get('distort_coeffs', []),
|
||||
'depth_scale': 1.0, # 不缩放深度
|
||||
}
|
||||
else:
|
||||
calib = None
|
||||
print("警告:未提供标定文件,无法进行3D可视化")
|
||||
|
||||
# 默认保存所有类型
|
||||
if save_types is None:
|
||||
save_types = ['2d', '3d', 'bev', 'combined']
|
||||
|
||||
output_filename = Path(image_path).stem
|
||||
|
||||
# ========== 2D可视化 ==========
|
||||
img_2d = None
|
||||
if '2d' in save_types or 'combined' in save_types:
|
||||
print("\n生成2D可视化...")
|
||||
img_2d = plot_2d_boxes(img_processed, labels_processed, names=names, label_text="2D GT", scale_factor=1)
|
||||
|
||||
# 保存2D可视化
|
||||
if '2d' in save_types:
|
||||
save_path_2d = output_dir / f"{output_filename}_2d_gt.jpg"
|
||||
cv2.imwrite(str(save_path_2d), img_2d)
|
||||
print(f"已保存2D可视化: {save_path_2d}")
|
||||
|
||||
# ========== 3D可视化 ==========
|
||||
img_3d_bgr = None
|
||||
bev_image = None
|
||||
decoded_results = []
|
||||
|
||||
need_3d = '3d' in save_types or 'combined' in save_types
|
||||
need_bev = 'bev' in save_types or 'combined' in save_types
|
||||
|
||||
if calib is not None and len(labels_processed) > 0 and (need_3d or need_bev):
|
||||
print("\n生成3D可视化...")
|
||||
|
||||
# 准备图像张量(BGR格式,归一化到[0,1])
|
||||
img_tensor = torch.from_numpy(img_processed.transpose(2, 0, 1)).float() / 255.0
|
||||
img_tensor = img_tensor.unsqueeze(0) # (1, 3, H, W)
|
||||
|
||||
h_img, w_img = img_processed.shape[:2]
|
||||
|
||||
# 解码3D框
|
||||
decoded_results = []
|
||||
for label in labels_processed:
|
||||
# 在decode函数中需要添加batch_idx列(列0)
|
||||
target = np.concatenate([[0], label]) # (48,)
|
||||
|
||||
decoded = decode_and_reconstruct_3d_box_from_target(
|
||||
target, calib, w_img, h_img
|
||||
)
|
||||
if decoded is not None:
|
||||
decoded_results.append(decoded)
|
||||
|
||||
print(f"成功解码 {len(decoded_results)} 个3D框")
|
||||
|
||||
if len(decoded_results) > 0:
|
||||
# 3D投影可视化
|
||||
if need_3d:
|
||||
img_3d = plot_3d_boxes_from_decoded_targets(
|
||||
img_tensor,
|
||||
[decoded_results],
|
||||
[str(image_path)],
|
||||
calib=[calib],
|
||||
names=names,
|
||||
label_text="3D GT",
|
||||
scale_factor=2
|
||||
)
|
||||
|
||||
if img_3d is not None:
|
||||
# 转换为BGR
|
||||
img_3d_bgr = cv2.cvtColor(img_3d, cv2.COLOR_RGB2BGR)
|
||||
|
||||
# 保存3D可视化
|
||||
if '3d' in save_types:
|
||||
save_path_3d = output_dir / f"{output_filename}_3d_gt.jpg"
|
||||
cv2.imwrite(str(save_path_3d), img_3d_bgr)
|
||||
print(f"已保存3D可视化: {save_path_3d}")
|
||||
|
||||
# BEV可视化
|
||||
if need_bev:
|
||||
print("\n生成BEV可视化...")
|
||||
bev_image = draw_bev_blank()
|
||||
for decoded in decoded_results:
|
||||
if decoded and decoded.get('object_3d') is not None:
|
||||
bev_image = drawbev(bev_image, decoded['object_3d'], is_pred=False)
|
||||
|
||||
# 保存BEV可视化
|
||||
if 'bev' in save_types:
|
||||
save_path_bev = output_dir / f"{output_filename}_bev_gt.jpg"
|
||||
cv2.imwrite(str(save_path_bev), bev_image)
|
||||
print(f"已保存BEV可视化: {save_path_bev}")
|
||||
|
||||
# 生成组合图像(2D + 3D 上下组合,然后与 BEV 左右组合)
|
||||
if 'combined' in save_types and img_2d is not None and img_3d_bgr is not None and bev_image is not None:
|
||||
print("\n生成组合可视化...")
|
||||
|
||||
# 步骤1: 确保2D和3D宽度一致
|
||||
h_2d, w_2d = img_2d.shape[:2]
|
||||
h_3d, w_3d = img_3d_bgr.shape[:2]
|
||||
|
||||
# 以2D的宽度为准,调整3D图像
|
||||
if w_3d != w_2d:
|
||||
h_3d_resized = int(h_3d * w_2d / w_3d)
|
||||
img_3d_bgr_resized = cv2.resize(img_3d_bgr, (w_2d, h_3d_resized))
|
||||
else:
|
||||
img_3d_bgr_resized = img_3d_bgr
|
||||
h_3d_resized = h_3d
|
||||
|
||||
# 步骤2: 添加垂直间隔并上下拼接2D和3D
|
||||
padding_height = 20
|
||||
padding_vertical = np.zeros((padding_height, w_2d, 3), dtype=np.uint8)
|
||||
left_side = np.vstack([img_2d, padding_vertical, img_3d_bgr_resized])
|
||||
|
||||
# 步骤3: 调整BEV高度以匹配左侧总高度
|
||||
h_left, w_left = left_side.shape[:2]
|
||||
h_bev, w_bev = bev_image.shape[:2]
|
||||
|
||||
# 保持BEV的宽高比,调整高度匹配左侧
|
||||
w_bev_resized = int(w_bev * h_left / h_bev)
|
||||
bev_image_resized = cv2.resize(bev_image, (w_bev_resized, h_left))
|
||||
|
||||
# 步骤4: 添加水平间隔并左右拼接
|
||||
padding_width = 20
|
||||
padding_horizontal = np.zeros((h_left, padding_width, 3), dtype=np.uint8)
|
||||
combined = np.hstack([left_side, padding_horizontal, bev_image_resized])
|
||||
|
||||
save_path_combined = output_dir / f"{output_filename}_combined.jpg"
|
||||
cv2.imwrite(str(save_path_combined), combined)
|
||||
print(f"已保存组合可视化: {save_path_combined}")
|
||||
else:
|
||||
print("警告:没有成功解码的3D框")
|
||||
else:
|
||||
if calib is None:
|
||||
print("跳过3D可视化:未提供标定文件")
|
||||
else:
|
||||
print("跳过3D可视化:没有标签")
|
||||
|
||||
print(f"\n可视化完成!输出目录: {output_dir}")
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description="单帧图像真值可视化脚本")
|
||||
parser.add_argument("--image", type=str, required=True, help="图像路径")
|
||||
parser.add_argument("--label", type=str, required=True, help="标签文件路径")
|
||||
parser.add_argument("--output", type=str, default="./gt_visualization", help="输出目录")
|
||||
parser.add_argument("--calib", type=str, default=None,
|
||||
help="相机标定文件路径(camera4.json),如不提供则从图像路径推断")
|
||||
parser.add_argument("--roi", type=int, nargs=2, default=None, metavar=("WIDTH", "HEIGHT"),
|
||||
help="ROI尺寸(宽 高),如: --roi 704 352")
|
||||
parser.add_argument("--virtual-fx", type=float, default=None,
|
||||
help="虚拟焦距,用于深度归一化")
|
||||
parser.add_argument("--ori-img-size", type=int, nargs=2, default=None, metavar=("WIDTH", "HEIGHT"),
|
||||
help="原始图像尺寸(宽 高),如不提供则从图像自动获取")
|
||||
parser.add_argument("--classes", type=str, nargs="+", default=None,
|
||||
help="仅可视化指定类别,支持类别名/类别ID,可传多个值或逗号分隔")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 如果没有提供calib路径,尝试从图像路径推断
|
||||
if args.calib is None:
|
||||
image_path = Path(args.image)
|
||||
base = image_path.parent.parent # dataset root
|
||||
# 兼容 calib/L2_calib/camera4.json 和 calib/camera4.json 两种目录结构
|
||||
for calib_path in [
|
||||
base / 'calib' / 'L2_calib' / 'camera4.json',
|
||||
base / 'calib' / 'camera4.json',
|
||||
]:
|
||||
if calib_path.exists():
|
||||
args.calib = str(calib_path)
|
||||
print(f"自动找到标定文件: {args.calib}")
|
||||
break
|
||||
else:
|
||||
print(f"警告:未找到标定文件(已检查 calib/L2_calib/camera4.json 和 calib/camera4.json),将仅进行2D可视化")
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
|
||||
names = DEFAULT_CLASS_NAMES.copy()
|
||||
target_class_ids = resolve_target_class_ids(args.classes, names=names)
|
||||
|
||||
visualize_single_frame(
|
||||
image_path=args.image,
|
||||
label_path=args.label,
|
||||
output_dir=args.output,
|
||||
calib_path=args.calib,
|
||||
roi_size=tuple(args.roi) if args.roi else None,
|
||||
virtual_fx=args.virtual_fx,
|
||||
ori_img_size=tuple(args.ori_img_size) if args.ori_img_size else None,
|
||||
names=names,
|
||||
target_class_ids=target_class_ids,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user