|
# S3DIS 数据集 |
|
|
|
## 数据集的准备 |
|
|
|
对于数据集准备的整体流程,请参考 S3DIS 的[指南](https://github.com/open-mmlab/mmdetection3d/blob/master/data/s3dis/README.md/)。 |
|
|
|
### 提取 S3DIS 数据 |
|
|
|
通过从原始数据中提取 S3DIS 数据,我们将点云数据读取并保存下相关的标注信息,例如语义分割标签和实例分割标签。 |
|
|
|
数据提取前的目录结构应该如下所示: |
|
|
|
``` |
|
mmdetection3d |
|
├── mmdet3d |
|
├── tools |
|
├── configs |
|
├── data |
|
│ ├── s3dis |
|
│ │ ├── meta_data |
|
│ │ ├── Stanford3dDataset_v1.2_Aligned_Version |
|
│ │ │ ├── Area_1 |
|
│ │ │ │ ├── conferenceRoom_1 |
|
│ │ │ │ ├── office_1 |
|
│ │ │ │ ├── ... |
|
│ │ │ ├── Area_2 |
|
│ │ │ ├── Area_3 |
|
│ │ │ ├── Area_4 |
|
│ │ │ ├── Area_5 |
|
│ │ │ ├── Area_6 |
|
│ │ ├── indoor3d_util.py |
|
│ │ ├── collect_indoor3d_data.py |
|
│ │ ├── README.md |
|
``` |
|
|
|
在 `Stanford3dDataset_v1.2_Aligned_Version` 目录下,所有房间依据所属区域被分为 6 组。 |
|
我们通常使用 5 个区域进行训练,然后在余下 1 个区域上进行测试 (被余下的 1 个区域通常为区域 5)。 |
|
在每个区域的目录下包含有多个房间的文件夹,每个文件夹是一个房间的原始点云数据和相关的标注信息。 |
|
例如,在 `Area_1/office_1` 目录下的文件如下所示: |
|
|
|
- `office_1.txt`:一个 txt 文件存储着原始点云数据每个点的坐标和颜色信息。 |
|
|
|
- `Annotations/`:这个文件夹里包含有此房间中实例物体的信息 (以 txt 文件的形式存储)。每个 txt 文件表示一个实例,例如: |
|
|
|
- `chair_1.txt`:存储有该房间中一把椅子的点云数据。 |
|
|
|
如果我们将 `Annotations/` 下的所有 txt 文件合并起来,得到的点云就和 `office_1.txt` 中的点云是一致的。 |
|
|
|
你可以通过 `python collect_indoor3d_data.py` 指令进行 S3DIS 数据的提取。 |
|
主要步骤包括: |
|
|
|
- 从原始 txt 文件中读取点云数据、语义分割标签和实例分割标签。 |
|
- 将点云数据和相关标注文件存储下来。 |
|
|
|
这其中的核心函数 `indoor3d_util.py` 中的 `export` 函数实现如下: |
|
|
|
```python |
|
def export(anno_path, out_filename): |
|
"""将原始数据集的文件转化为点云、语义分割标签和实例分割掩码文件。 |
|
我们将同一房间中所有实例的点进行聚合。 |
|
|
|
参数列表: |
|
anno_path (str): 标注信息的路径,例如 Area_1/office_2/Annotations/ |
|
out_filename (str): 保存点云和标签的路径 |
|
file_format (str): txt 或 numpy,指定保存的文件格式 |
|
|
|
注意: |
|
点云在处理过程中被整体移动了,保存下的点最小位于原点 (即没有负数坐标值) |
|
""" |
|
points_list = [] |
|
ins_idx = 1 # 实例标签从 1 开始,因此最终实例标签为 0 的点就是无标注的点 |
|
|
|
# `anno_path` 的一个例子:Area_1/office_1/Annotations |
|
# 其中以 txt 文件存储有该房间中所有实例物体的点云 |
|
for f in glob.glob(osp.join(anno_path, '*.txt')): |
|
# get class name of this instance |
|
one_class = osp.basename(f).split('_')[0] |
|
if one_class not in class_names: # 某些房间有 'staris' 类物体 |
|
one_class = 'clutter' |
|
points = np.loadtxt(f) |
|
labels = np.ones((points.shape[0], 1)) * class2label[one_class] |
|
ins_labels = np.ones((points.shape[0], 1)) * ins_idx |
|
ins_idx += 1 |
|
points_list.append(np.concatenate([points, labels, ins_labels], 1)) |
|
|
|
data_label = np.concatenate(points_list, 0) # [N, 8], (pts, rgb, sem, ins) |
|
# 将点云对齐到原点 |
|
xyz_min = np.amin(data_label, axis=0)[0:3] |
|
data_label[:, 0:3] -= xyz_min |
|
|
|
np.save(f'{out_filename}_point.npy', data_label[:, :6].astype(np.float32)) |
|
np.save(f'{out_filename}_sem_label.npy', data_label[:, 6].astype(np.int64)) |
|
np.save(f'{out_filename}_ins_label.npy', data_label[:, 7].astype(np.int64)) |
|
|
|
``` |
|
|
|
上述代码中,我们读取 `Annotations/` 下的所有点云实例,将其合并得到整体房屋的点云,同时生成语义/实例分割的标签。 |
|
在提取完每个房间的数据后,点云、语义分割和实例分割的标签文件应以 `.npy` 的格式被保存下来。 |
|
|
|
### 创建数据集 |
|
|
|
```shell |
|
python tools/create_data.py s3dis --root-path ./data/s3dis \ |
|
--out-dir ./data/s3dis --extra-tag s3dis |
|
``` |
|
|
|
上述指令首先读取以 `.npy` 格式存储的点云、语义分割和实例分割标签文件,然后进一步将它们以 `.bin` 格式保存。 |
|
同时,每个区域 `.pkl` 格式的信息文件也会被保存下来。 |
|
|
|
数据预处理后的目录结构如下所示: |
|
|
|
``` |
|
s3dis |
|
├── meta_data |
|
├── indoor3d_util.py |
|
├── collect_indoor3d_data.py |
|
├── README.md |
|
├── Stanford3dDataset_v1.2_Aligned_Version |
|
├── s3dis_data |
|
├── points |
|
│ ├── xxxxx.bin |
|
├── instance_mask |
|
│ ├── xxxxx.bin |
|
├── semantic_mask |
|
│ ├── xxxxx.bin |
|
├── seg_info |
|
│ ├── Area_1_label_weight.npy |
|
│ ├── Area_1_resampled_scene_idxs.npy |
|
│ ├── Area_2_label_weight.npy |
|
│ ├── Area_2_resampled_scene_idxs.npy |
|
│ ├── Area_3_label_weight.npy |
|
│ ├── Area_3_resampled_scene_idxs.npy |
|
│ ├── Area_4_label_weight.npy |
|
│ ├── Area_4_resampled_scene_idxs.npy |
|
│ ├── Area_5_label_weight.npy |
|
│ ├── Area_5_resampled_scene_idxs.npy |
|
│ ├── Area_6_label_weight.npy |
|
│ ├── Area_6_resampled_scene_idxs.npy |
|
├── s3dis_infos_Area_1.pkl |
|
├── s3dis_infos_Area_2.pkl |
|
├── s3dis_infos_Area_3.pkl |
|
├── s3dis_infos_Area_4.pkl |
|
├── s3dis_infos_Area_5.pkl |
|
├── s3dis_infos_Area_6.pkl |
|
``` |
|
|
|
- `points/xxxxx.bin`:提取的点云数据。 |
|
- `instance_mask/xxxxx.bin`:每个点云的实例标签,取值范围为 \[0, ${实例个数}\],其中 0 代表未标注的点。 |
|
- `semantic_mask/xxxxx.bin`:每个点云的语义标签,取值范围为 \[0, 12\]。 |
|
- `s3dis_infos_Area_1.pkl`:区域 1 的数据信息,每个房间的详细信息如下: |
|
- info\['point_cloud'\]: {'num_features': 6, 'lidar_idx': sample_idx}. |
|
- info\['pts_path'\]: `points/xxxxx.bin` 点云的路径。 |
|
- info\['pts_instance_mask_path'\]: `instance_mask/xxxxx.bin` 实例标签的路径。 |
|
- info\['pts_semantic_mask_path'\]: `semantic_mask/xxxxx.bin` 语义标签的路径。 |
|
- `seg_info`:为支持语义分割任务所生成的信息文件。 |
|
- `Area_1_label_weight.npy`:每一语义类别的权重系数。因为 S3DIS 中属于不同类的点的数量相差很大,一个常见的操作是在计算损失时对不同类别进行加权 (label re-weighting) 以得到更好的分割性能。 |
|
- `Area_1_resampled_scene_idxs.npy`:每一个场景 (房间) 的重采样标签。在训练过程中,我们依据每个场景的点的数量,会对其进行不同次数的重采样,以保证训练数据均衡。 |
|
|
|
## 训练流程 |
|
|
|
S3DIS 上 3D 语义分割的一种典型数据载入流程如下所示: |
|
|
|
```python |
|
class_names = ('ceiling', 'floor', 'wall', 'beam', 'column', 'window', 'door', |
|
'table', 'chair', 'sofa', 'bookcase', 'board', 'clutter') |
|
num_points = 4096 |
|
train_pipeline = [ |
|
dict( |
|
type='LoadPointsFromFile', |
|
coord_type='DEPTH', |
|
shift_height=False, |
|
use_color=True, |
|
load_dim=6, |
|
use_dim=[0, 1, 2, 3, 4, 5]), |
|
dict( |
|
type='LoadAnnotations3D', |
|
with_bbox_3d=False, |
|
with_label_3d=False, |
|
with_mask_3d=False, |
|
with_seg_3d=True), |
|
dict( |
|
type='PointSegClassMapping'), |
|
dict( |
|
type='IndoorPatchPointSample', |
|
num_points=num_points, |
|
block_size=1.0, |
|
ignore_index=None, |
|
use_normalized_coord=True, |
|
enlarge_size=None, |
|
min_unique_num=num_points // 4, |
|
eps=0.0), |
|
dict(type='NormalizePointsColor', color_mean=None), |
|
dict( |
|
type='GlobalRotScaleTrans', |
|
rot_range=[-3.141592653589793, 3.141592653589793], # [-pi, pi] |
|
scale_ratio_range=[0.8, 1.2], |
|
translation_std=[0, 0, 0]), |
|
dict( |
|
type='RandomJitterPoints', |
|
jitter_std=[0.01, 0.01, 0.01], |
|
clip_range=[-0.05, 0.05]), |
|
dict(type='RandomDropPointsColor', drop_ratio=0.2), |
|
dict(type='Pack3DDetInputs', keys=['points', 'pts_semantic_mask']) |
|
] |
|
``` |
|
|
|
- `PointSegClassMapping`:在训练过程中,只有被使用的类别的序号会被映射到类似 \[0, 13) 范围内的类别标签。其余的类别序号会被转换为 `ignore_index` 所制定的忽略标签,在本例中是 `13`。 |
|
- `IndoorPatchPointSample`:从输入点云中裁剪一个含有固定数量点的小块 (patch)。`block_size` 指定了裁剪块的边长,在 S3DIS 上这个数值一般设置为 `1.0`。 |
|
- `NormalizePointsColor`:将输入点的颜色信息归一化,通过将 RGB 值除以 `255` 来实现。 |
|
- 数据增广: |
|
- `GlobalRotScaleTrans`:对输入点云进行随机旋转和放缩变换。 |
|
- `RandomJitterPoints`:通过对每一个点施加不同的噪声向量以实现对点云的随机扰动。 |
|
- `RandomDropPointsColor`:以 `drop_ratio` 的概率随机将点云的颜色值全部置零。 |
|
|
|
## 度量指标 |
|
|
|
通常我们使用平均交并比 (mean Intersection over Union, mIoU) 作为 S3DIS 语义分割任务的度量指标。 |
|
具体而言,我们先计算所有类别的 IoU,然后取平均值作为 mIoU。 |
|
更多实现细节请参考 [seg_eval.py](https://github.com/open-mmlab/mmdetection3d/blob/dev-1.x/mmdet3d/evaluation/functional/seg_eval.py)。 |
|
|
|
正如在 `提取 S3DIS 数据` 一节中所提及的,S3DIS 通常在 5 个区域上进行训练,然后在余下的 1 个区域上进行测试。但是在其他论文中,也有不同的划分方式。 |
|
为了便于灵活划分训练和测试的子集,我们首先定义子数据集 (sub-dataset) 来表示每一个区域,然后根据区域划分对其进行合并,以得到完整的训练集。 |
|
以下是在区域 1、2、3、4、6 上训练并在区域 5 上测试的一个配置文件例子: |
|
|
|
```python |
|
dataset_type = 'S3DISSegDataset' |
|
data_root = './data/s3dis/' |
|
class_names = ('ceiling', 'floor', 'wall', 'beam', 'column', 'window', 'door', |
|
'table', 'chair', 'sofa', 'bookcase', 'board', 'clutter') |
|
train_area = [1, 2, 3, 4, 6] |
|
test_area = 5 |
|
train_dataloader = dict( |
|
batch_size=8, |
|
num_workers=4, |
|
persistent_workers=True, |
|
sampler=dict(type='DefaultSampler', shuffle=True), |
|
dataset=dict( |
|
type=dataset_type, |
|
data_root=data_root, |
|
ann_files=[f's3dis_infos_Area_{i}.pkl' for i in train_area], |
|
metainfo=metainfo, |
|
data_prefix=data_prefix, |
|
pipeline=train_pipeline, |
|
modality=input_modality, |
|
ignore_index=len(class_names), |
|
scene_idxs=[ |
|
f'seg_info/Area_{i}_resampled_scene_idxs.npy' for i in train_area |
|
], |
|
test_mode=False)) |
|
test_dataloader = dict( |
|
batch_size=1, |
|
num_workers=1, |
|
persistent_workers=True, |
|
drop_last=False, |
|
sampler=dict(type='DefaultSampler', shuffle=False), |
|
dataset=dict( |
|
type=dataset_type, |
|
data_root=data_root, |
|
ann_files=f's3dis_infos_Area_{test_area}.pkl', |
|
metainfo=metainfo, |
|
data_prefix=data_prefix, |
|
pipeline=test_pipeline, |
|
modality=input_modality, |
|
ignore_index=len(class_names), |
|
scene_idxs=f'seg_info/Area_{test_area}_resampled_scene_idxs.npy', |
|
test_mode=True)) |
|
val_dataloader = test_dataloader |
|
``` |
|
|
|
可以看到,我们通过将多个相应路径构成的列表 (list) 输入 `ann_files` 和 `scene_idxs` 以实现训练测试集的划分。 |
|
如果修改训练测试区域的划分,只需要简单修改 `train_area` 和 `test_area` 即可。 |
|
|