openpoints 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openpoints/__init__.py +3 -0
- openpoints/cpp/__init__.py +6 -0
- openpoints/cpp/chamfer_dist/__init__.py +110 -0
- openpoints/cpp/chamfer_dist/setup.py +19 -0
- openpoints/cpp/chamfer_dist/test.py +34 -0
- openpoints/cpp/emd/__init__.py +3 -0
- openpoints/cpp/emd/emd.py +88 -0
- openpoints/cpp/emd/setup.py +27 -0
- openpoints/cpp/emd/test_emd_loss.py +45 -0
- openpoints/cpp/pointnet2_batch/__init__.py +24 -0
- openpoints/cpp/pointnet2_batch/setup.py +23 -0
- openpoints/cpp/pointops/__init__.py +0 -0
- openpoints/cpp/pointops/functions/__init__.py +0 -0
- openpoints/cpp/pointops/functions/pointops.py +314 -0
- openpoints/cpp/pointops/setup.py +37 -0
- openpoints/cpp/pointops/src/__init__.py +0 -0
- openpoints/dataset/__init__.py +10 -0
- openpoints/dataset/atom3d/__init__.py +1 -0
- openpoints/dataset/atom3d/psr.py +38 -0
- openpoints/dataset/build.py +98 -0
- openpoints/dataset/data_util.py +192 -0
- openpoints/dataset/datalist.py +67 -0
- openpoints/dataset/dataset_base.py +96 -0
- openpoints/dataset/graph_dataset/__init__.py +3 -0
- openpoints/dataset/graph_dataset/graph_dataset.py +93 -0
- openpoints/dataset/graph_dataset/stack_with_pad.py +91 -0
- openpoints/dataset/graph_dataset/structural_dataset.py +73 -0
- openpoints/dataset/graph_dataset/svd_encodings_dataset.py +110 -0
- openpoints/dataset/grid_sample.py +21 -0
- openpoints/dataset/matterport3d/__init__.py +1 -0
- openpoints/dataset/matterport3d/category_mapping.tsv +1660 -0
- openpoints/dataset/matterport3d/matterport3d.py +210 -0
- openpoints/dataset/matterport3d/matterport3d_dataprocessing.py +105 -0
- openpoints/dataset/modelnet/__init__.py +3 -0
- openpoints/dataset/modelnet/modelnet40_normal_resampled_loader.py +124 -0
- openpoints/dataset/modelnet/modelnet40_ply_2048_loader.py +160 -0
- openpoints/dataset/molhiv/__init__.py +1 -0
- openpoints/dataset/molhiv/data.py +59 -0
- openpoints/dataset/molpcba/__init__.py +1 -0
- openpoints/dataset/molpcba/data.py +59 -0
- openpoints/dataset/parsers/__init__.py +1 -0
- openpoints/dataset/parsers/class_map.py +19 -0
- openpoints/dataset/parsers/constants.py +1 -0
- openpoints/dataset/parsers/parser.py +17 -0
- openpoints/dataset/parsers/parser_factory.py +29 -0
- openpoints/dataset/parsers/parser_image_folder.py +69 -0
- openpoints/dataset/parsers/parser_image_in_tar.py +222 -0
- openpoints/dataset/parsers/parser_image_tar.py +72 -0
- openpoints/dataset/parsers/parser_tfds.py +297 -0
- openpoints/dataset/pcqm4m/__init__.py +1 -0
- openpoints/dataset/pcqm4m/data.py +62 -0
- openpoints/dataset/pcqm4mv2/__init__.py +1 -0
- openpoints/dataset/pcqm4mv2/data.py +87 -0
- openpoints/dataset/s3dis/__init__.py +2 -0
- openpoints/dataset/s3dis/s3dis.py +156 -0
- openpoints/dataset/s3dis/s3dis_block.py +96 -0
- openpoints/dataset/s3dis/s3dis_sphere.py +349 -0
- openpoints/dataset/scannetv2/__init__.py +1 -0
- openpoints/dataset/scannetv2/scannet.py +176 -0
- openpoints/dataset/scanobjectnn/__init__.py +3 -0
- openpoints/dataset/scanobjectnn/scanobjectnn.py +110 -0
- openpoints/dataset/semantic_kitti/__init__.py +1 -0
- openpoints/dataset/semantic_kitti/helper_tool.py +286 -0
- openpoints/dataset/semantic_kitti/label_mapping.yaml +211 -0
- openpoints/dataset/semantic_kitti/semantickitti.py +229 -0
- openpoints/dataset/semantic_kitti/utils/meta/anno_paths.txt +272 -0
- openpoints/dataset/semantic_kitti/utils/meta/class_names.txt +13 -0
- openpoints/dataset/semantic_kitti/utils/semantic-kitti.yaml +211 -0
- openpoints/dataset/shapenet/__init__.py +1 -0
- openpoints/dataset/shapenet/shapenet55.py +76 -0
- openpoints/dataset/shapenet/shapenetpart.py +121 -0
- openpoints/dataset/shapenetpart/__init__.py +1 -0
- openpoints/dataset/shapenetpart/shapenet55.py +75 -0
- openpoints/dataset/shapenetpart/shapenetpart.py +388 -0
- openpoints/dataset/vis2d.py +17 -0
- openpoints/dataset/vis3d.py +153 -0
- openpoints/loss/__init__.py +3 -0
- openpoints/loss/build.py +281 -0
- openpoints/loss/cross_entropy.py +38 -0
- openpoints/loss/distill_loss.py +76 -0
- openpoints/models/__init__.py +10 -0
- openpoints/models/backbone/Stratified_transformer.py +558 -0
- openpoints/models/backbone/__init__.py +11 -0
- openpoints/models/backbone/baafnet.py +527 -0
- openpoints/models/backbone/ball_dgcnn.py +123 -0
- openpoints/models/backbone/curvenet.py +793 -0
- openpoints/models/backbone/debug_invvit.py +114 -0
- openpoints/models/backbone/deepgcn.py +143 -0
- openpoints/models/backbone/dgcnn.py +119 -0
- openpoints/models/backbone/graphvit3d.py +134 -0
- openpoints/models/backbone/grouppointnet.py +100 -0
- openpoints/models/backbone/pct.py +163 -0
- openpoints/models/backbone/pointmlp.py +417 -0
- openpoints/models/backbone/pointnet.py +199 -0
- openpoints/models/backbone/pointnetv2.py +511 -0
- openpoints/models/backbone/pointnext.py +663 -0
- openpoints/models/backbone/pointnextPyG.py +555 -0
- openpoints/models/backbone/pointtransformer.py +293 -0
- openpoints/models/backbone/pointvector.py +853 -0
- openpoints/models/backbone/pointvit.py +392 -0
- openpoints/models/backbone/pointvit_inv.py +942 -0
- openpoints/models/backbone/pointvit_inv_old.py +784 -0
- openpoints/models/backbone/randlenet.py +318 -0
- openpoints/models/backbone/resnet.py +342 -0
- openpoints/models/backbone/simpleview.py +153 -0
- openpoints/models/backbone/simpleview_util.py +292 -0
- openpoints/models/build.py +13 -0
- openpoints/models/classification/__init__.py +5 -0
- openpoints/models/classification/cls_base.py +136 -0
- openpoints/models/classification/point_bert.py +154 -0
- openpoints/models/layers/__init__.py +14 -0
- openpoints/models/layers/activation.py +57 -0
- openpoints/models/layers/attention.py +103 -0
- openpoints/models/layers/conv.py +167 -0
- openpoints/models/layers/drop.py +164 -0
- openpoints/models/layers/graph_conv.py +122 -0
- openpoints/models/layers/group.py +415 -0
- openpoints/models/layers/group_embed.py +286 -0
- openpoints/models/layers/helpers.py +43 -0
- openpoints/models/layers/kmeans.py +119 -0
- openpoints/models/layers/knn.py +110 -0
- openpoints/models/layers/local_aggregation.py +286 -0
- openpoints/models/layers/mlp.py +129 -0
- openpoints/models/layers/norm.py +106 -0
- openpoints/models/layers/padding.py +56 -0
- openpoints/models/layers/patch_embed.py +37 -0
- openpoints/models/layers/registry.py +168 -0
- openpoints/models/layers/subsample.py +185 -0
- openpoints/models/layers/upsampling.py +106 -0
- openpoints/models/layers/weight_init.py +89 -0
- openpoints/models/reconstruction/__init__.py +8 -0
- openpoints/models/reconstruction/base_recontruct.py +216 -0
- openpoints/models/reconstruction/maskedpoint.py +116 -0
- openpoints/models/reconstruction/maskedpointgroup.py +168 -0
- openpoints/models/reconstruction/maskedpointvit.py +253 -0
- openpoints/models/reconstruction/nodeshuffle.py +11 -0
- openpoints/models/registry.py +149 -0
- openpoints/models/segmentation/__init__.py +6 -0
- openpoints/models/segmentation/base_seg.py +278 -0
- openpoints/models/segmentation/vit_seg.py +126 -0
- openpoints/optim/__init__.py +15 -0
- openpoints/optim/adabelief.py +201 -0
- openpoints/optim/adafactor.py +167 -0
- openpoints/optim/adahessian.py +156 -0
- openpoints/optim/adamp.py +105 -0
- openpoints/optim/adamw.py +122 -0
- openpoints/optim/lamb.py +192 -0
- openpoints/optim/lars.py +135 -0
- openpoints/optim/lookahead.py +61 -0
- openpoints/optim/madgrad.py +184 -0
- openpoints/optim/nadam.py +92 -0
- openpoints/optim/nvnovograd.py +120 -0
- openpoints/optim/optim_factory.py +306 -0
- openpoints/optim/radam.py +89 -0
- openpoints/optim/rmsprop_tf.py +139 -0
- openpoints/optim/sgdp.py +70 -0
- openpoints/scheduler/__init__.py +8 -0
- openpoints/scheduler/cosine_lr.py +124 -0
- openpoints/scheduler/multistep_lr.py +65 -0
- openpoints/scheduler/plateau_lr.py +113 -0
- openpoints/scheduler/poly_lr.py +116 -0
- openpoints/scheduler/scheduler.py +110 -0
- openpoints/scheduler/scheduler_factory.py +118 -0
- openpoints/scheduler/step_lr.py +65 -0
- openpoints/scheduler/tanh_lr.py +117 -0
- openpoints/transforms/__init__.py +7 -0
- openpoints/transforms/point_transform_cpu.py +332 -0
- openpoints/transforms/point_transformer_gpu.py +764 -0
- openpoints/transforms/transforms_factory.py +60 -0
- openpoints/utils/__init__.py +8 -0
- openpoints/utils/ckpt_util.py +438 -0
- openpoints/utils/config.py +113 -0
- openpoints/utils/dist_utils.py +54 -0
- openpoints/utils/logger.py +170 -0
- openpoints/utils/metrics.py +311 -0
- openpoints/utils/random.py +16 -0
- openpoints/utils/registry.py +294 -0
- openpoints/utils/str2bool.py +11 -0
- openpoints/utils/wandb.py +88 -0
- openpoints-0.1.0.dist-info/METADATA +150 -0
- openpoints-0.1.0.dist-info/RECORD +184 -0
- openpoints-0.1.0.dist-info/WHEEL +5 -0
- openpoints-0.1.0.dist-info/licenses/LICENSE +21 -0
- openpoints-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.utils.data as data
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# class S3DIS(data.Dataset):
|
|
12
|
+
# r"""The (pre-processed) Stanford Large-Scale 3D Indoor Spaces dataset from
|
|
13
|
+
# the `"3D Semantic Parsing of Large-Scale Indoor Spaces"
|
|
14
|
+
# <https://openaccess.thecvf.com/content_cvpr_2016/papers/Armeni_3D_Semantic_Parsing_CVPR_2016_paper.pdf>`_
|
|
15
|
+
# paper, containing point clouds of six large-scale indoor parts in three
|
|
16
|
+
# buildings with 12 semantic elements (and one clutter class).
|
|
17
|
+
|
|
18
|
+
# Args:
|
|
19
|
+
# root (string): Root directory where the dataset should be saved.
|
|
20
|
+
# test_area (int, optional): Which area to use for testing (1-6).
|
|
21
|
+
# (default: :obj:`6`)
|
|
22
|
+
# train (bool, optional): If :obj:`True`, loads the training dataset,
|
|
23
|
+
# otherwise the test dataset. (default: :obj:`True`)
|
|
24
|
+
# transform (callable, optional): A function/transform that takes in an
|
|
25
|
+
# :obj:`torch_geometric.data.Data` object and returns a transformed
|
|
26
|
+
# version. The data object will be transformed before every access.
|
|
27
|
+
# (default: :obj:`None`)
|
|
28
|
+
# pre_transform (callable, optional): A function/transform that takes in
|
|
29
|
+
# an :obj:`torch_geometric.data.Data` object and returns a
|
|
30
|
+
# transformed version. The data object will be transformed before
|
|
31
|
+
# being saved to disk. (default: :obj:`None`)
|
|
32
|
+
# pre_filter (callable, optional): A function that takes in an
|
|
33
|
+
# :obj:`torch_geometric.data.Data` object and returns a boolean
|
|
34
|
+
# value, indicating whether the data object should be included in the
|
|
35
|
+
# final dataset. (default: :obj:`None`)
|
|
36
|
+
# """
|
|
37
|
+
|
|
38
|
+
# url = ('https://shapenet.cs.stanford.edu/media/'
|
|
39
|
+
# 'indoor3d_sem_seg_hdf5_data.zip')
|
|
40
|
+
|
|
41
|
+
# def __init__(self, root, test_area=6, train=True, transform=None,
|
|
42
|
+
# pre_transform=None, pre_filter=None):
|
|
43
|
+
# assert test_area >= 1 and test_area <= 6
|
|
44
|
+
# self.test_area = test_area
|
|
45
|
+
# super().__init__(root, transform, pre_transform, pre_filter)
|
|
46
|
+
# path = self.processed_paths[0] if train else self.processed_paths[1]
|
|
47
|
+
# self.data, self.slices = torch.load(path)
|
|
48
|
+
|
|
49
|
+
# @property
|
|
50
|
+
# def raw_file_names(self):
|
|
51
|
+
# return ['all_files.txt', 'room_filelist.txt']
|
|
52
|
+
|
|
53
|
+
# @property
|
|
54
|
+
# def processed_file_names(self):
|
|
55
|
+
# return [f'{split}_{self.test_area}.pt' for split in ['train', 'test']]
|
|
56
|
+
|
|
57
|
+
# def download(self):
|
|
58
|
+
# path = download_url(self.url, self.root)
|
|
59
|
+
# extract_zip(path, self.root)
|
|
60
|
+
# os.unlink(path)
|
|
61
|
+
# shutil.rmtree(self.raw_dir)
|
|
62
|
+
# name = self.url.split('/')[-1].split('.')[0]
|
|
63
|
+
# os.rename(osp.join(self.root, name), self.raw_dir)
|
|
64
|
+
|
|
65
|
+
# def process(self):
|
|
66
|
+
# import h5py
|
|
67
|
+
|
|
68
|
+
# with open(self.raw_paths[0], 'r') as f:
|
|
69
|
+
# filenames = [x.split('/')[-1] for x in f.read().split('\n')[:-1]]
|
|
70
|
+
|
|
71
|
+
# with open(self.raw_paths[1], 'r') as f:
|
|
72
|
+
# rooms = f.read().split('\n')[:-1]
|
|
73
|
+
|
|
74
|
+
# xs, ys = [], []
|
|
75
|
+
# for filename in filenames:
|
|
76
|
+
# f = h5py.File(osp.join(self.raw_dir, filename))
|
|
77
|
+
# xs += torch.from_numpy(f['data'][:]).unbind(0)
|
|
78
|
+
# ys += torch.from_numpy(f['label'][:]).to(torch.long).unbind(0)
|
|
79
|
+
|
|
80
|
+
# test_area = f'Area_{self.test_area}'
|
|
81
|
+
# train_data_list, test_data_list = [], []
|
|
82
|
+
# for i, (x, y) in enumerate(zip(xs, ys)):
|
|
83
|
+
# data = Data(pos=x[:, :3], x=x[:, 3:], y=y)
|
|
84
|
+
|
|
85
|
+
# if self.pre_filter is not None and not self.pre_filter(data):
|
|
86
|
+
# continue
|
|
87
|
+
# if self.pre_transform is not None:
|
|
88
|
+
# data = self.pre_transform(data)
|
|
89
|
+
|
|
90
|
+
# if test_area not in rooms[i]:
|
|
91
|
+
# train_data_list.append(data)
|
|
92
|
+
# else:
|
|
93
|
+
# test_data_list.append(data)
|
|
94
|
+
|
|
95
|
+
# torch.save(self.collate(train_data_list), self.processed_paths[0])
|
|
96
|
+
# torch.save(self.collate(test_data_list), self.processed_paths[1])
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.utils.data as data
|
|
3
|
+
import numpy as np
|
|
4
|
+
import os
|
|
5
|
+
import pickle
|
|
6
|
+
from sklearn.neighbors import KDTree
|
|
7
|
+
from ..build import DATASETS
|
|
8
|
+
from ..data_util import crop_pc, voxelize
|
|
9
|
+
from tqdm import tqdm
|
|
10
|
+
import logging
|
|
11
|
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@DATASETS.register_module()
|
|
15
|
+
class S3DISSphere(data.Dataset):
|
|
16
|
+
label_to_names = {0: 'ceiling',
|
|
17
|
+
1: 'floor',
|
|
18
|
+
2: 'wall',
|
|
19
|
+
3: 'beam',
|
|
20
|
+
4: 'column',
|
|
21
|
+
5: 'window',
|
|
22
|
+
6: 'door',
|
|
23
|
+
7: 'chair',
|
|
24
|
+
8: 'table',
|
|
25
|
+
9: 'bookcase',
|
|
26
|
+
10: 'sofa',
|
|
27
|
+
11: 'board',
|
|
28
|
+
12: 'clutter'}
|
|
29
|
+
name_to_label = {v: i for i, v in enumerate(label_to_names.values())}
|
|
30
|
+
classes = list(label_to_names.values())
|
|
31
|
+
color_mean = np.array([0.5136457, 0.49523646, 0.44921124])
|
|
32
|
+
color_std = np.array([0.18308958, 0.18415008, 0.19252081])
|
|
33
|
+
num_classes = 13
|
|
34
|
+
class2color = {'ceiling': [0, 255, 0],
|
|
35
|
+
'floor': [0, 0, 255],
|
|
36
|
+
'wall': [0, 255, 255],
|
|
37
|
+
'beam': [255, 255, 0],
|
|
38
|
+
'column': [255, 0, 255],
|
|
39
|
+
'window': [100, 100, 255],
|
|
40
|
+
'door': [200, 200, 100],
|
|
41
|
+
'chair': [255, 0, 0],
|
|
42
|
+
'table': [170, 120, 200],
|
|
43
|
+
'bookcase': [10, 200, 100],
|
|
44
|
+
'sofa': [200, 100, 100],
|
|
45
|
+
'board': [200, 200, 200],
|
|
46
|
+
'clutter': [50, 50, 50]}
|
|
47
|
+
cmap = np.array([*class2color.values()]).astype(np.uint8)
|
|
48
|
+
gravity_dim = 2
|
|
49
|
+
"""S3DIS dataset for scene segmentation task.
|
|
50
|
+
Args:
|
|
51
|
+
voxel_size: grid length for pre-subsampling point clouds.
|
|
52
|
+
in_radius: radius of each input spheres.
|
|
53
|
+
num_points: max number of points for the input spheres.
|
|
54
|
+
num_steps: number of spheres for one training epoch.
|
|
55
|
+
num_epochs: total epochs.
|
|
56
|
+
data_root: root path for data.
|
|
57
|
+
transform: data transformations.
|
|
58
|
+
split: dataset split name.
|
|
59
|
+
"""
|
|
60
|
+
def __init__(self, voxel_size,
|
|
61
|
+
in_radius, num_points, num_steps, num_epochs,
|
|
62
|
+
data_root=None,
|
|
63
|
+
transform=None,
|
|
64
|
+
split='train',
|
|
65
|
+
centering=False,
|
|
66
|
+
**kwargs):
|
|
67
|
+
|
|
68
|
+
super().__init__()
|
|
69
|
+
self.epoch = 0
|
|
70
|
+
self.transform = transform
|
|
71
|
+
self.voxel_size = voxel_size
|
|
72
|
+
self.in_radius = in_radius
|
|
73
|
+
self.num_points = num_points
|
|
74
|
+
self.num_steps = num_steps
|
|
75
|
+
self.num_epochs = num_epochs
|
|
76
|
+
self.centering = centering
|
|
77
|
+
self.data_root = data_root
|
|
78
|
+
self.train_clouds = ['Area_1', 'Area_2', 'Area_3', 'Area_4', 'Area_6']
|
|
79
|
+
self.val_clouds = ['Area_5']
|
|
80
|
+
self.split = split
|
|
81
|
+
|
|
82
|
+
if split == 'train':
|
|
83
|
+
cloud_names = self.train_clouds
|
|
84
|
+
else:
|
|
85
|
+
cloud_names = self.val_clouds
|
|
86
|
+
|
|
87
|
+
processed_dir = os.path.join(data_root, 'processed')
|
|
88
|
+
if not os.path.exists(processed_dir):
|
|
89
|
+
os.makedirs(processed_dir)
|
|
90
|
+
|
|
91
|
+
# prepare data
|
|
92
|
+
filename = os.path.join(
|
|
93
|
+
processed_dir, f'{split}_{voxel_size:.3f}_data.pkl')
|
|
94
|
+
if not os.path.exists(filename):
|
|
95
|
+
cloud_points_list, cloud_points_color_list, cloud_points_label_list = [], [], []
|
|
96
|
+
sub_cloud_points_list, sub_cloud_points_label_list, sub_cloud_points_color_list = [], [], []
|
|
97
|
+
sub_cloud_tree_list = []
|
|
98
|
+
cloud_rooms_list = []
|
|
99
|
+
for cloud_idx, cloud_name in enumerate(cloud_names):
|
|
100
|
+
# Pass if the cloud has already been computed
|
|
101
|
+
# Get rooms of the current cloud
|
|
102
|
+
cloud_folder = os.path.join(data_root,cloud_name)
|
|
103
|
+
room_folders = [os.path.join(cloud_folder, room) for room in os.listdir(cloud_folder) if
|
|
104
|
+
os.path.isdir(os.path.join(cloud_folder, room))]
|
|
105
|
+
# Initiate containers
|
|
106
|
+
cloud_points = np.empty((0, 3), dtype=np.float32)
|
|
107
|
+
cloud_colors = np.empty((0, 3), dtype=np.float32)
|
|
108
|
+
cloud_classes = np.empty((0, 1), dtype=np.int32)
|
|
109
|
+
cloud_room_split = [0]
|
|
110
|
+
# Loop over rooms
|
|
111
|
+
for i, room_folder in enumerate(room_folders):
|
|
112
|
+
print(
|
|
113
|
+
'Cloud %s - Room %d/%d : %s' % (
|
|
114
|
+
cloud_name, i + 1, len(room_folders), room_folder.split('\\')[-1]))
|
|
115
|
+
room_npoints = []
|
|
116
|
+
for object_name in os.listdir(os.path.join(room_folder, 'Annotations')):
|
|
117
|
+
if object_name[-4:] == '.txt':
|
|
118
|
+
# Text file containing point of the object
|
|
119
|
+
object_file = os.path.join(room_folder, 'Annotations', object_name)
|
|
120
|
+
# Object class and ID
|
|
121
|
+
tmp = object_name[:-4].split('_')[0]
|
|
122
|
+
if tmp in self.name_to_label:
|
|
123
|
+
object_class = self.name_to_label[tmp]
|
|
124
|
+
elif tmp in ['stairs']:
|
|
125
|
+
object_class = self.name_to_label['clutter']
|
|
126
|
+
else:
|
|
127
|
+
raise ValueError(
|
|
128
|
+
'Unknown object name: ' + str(tmp))
|
|
129
|
+
# Read object points and colors
|
|
130
|
+
try:
|
|
131
|
+
object_data = np.loadtxt(object_file)
|
|
132
|
+
except:
|
|
133
|
+
logging.info(f"error in reading file {object_file}")
|
|
134
|
+
# Stack all data
|
|
135
|
+
room_npoints.append(len(object_data))
|
|
136
|
+
cloud_points = np.vstack(
|
|
137
|
+
(cloud_points, object_data[:, 0:3].astype(np.float32)))
|
|
138
|
+
cloud_colors = np.vstack(
|
|
139
|
+
(cloud_colors, object_data[:, 3:6].astype(np.uint8)))
|
|
140
|
+
object_classes = np.full(
|
|
141
|
+
(object_data.shape[0], 1), object_class, dtype=np.int32)
|
|
142
|
+
cloud_classes = np.vstack(
|
|
143
|
+
(cloud_classes, object_classes))
|
|
144
|
+
cloud_room_split.append(cloud_room_split[-1]+sum(room_npoints))
|
|
145
|
+
cloud_points_list.append(cloud_points)
|
|
146
|
+
cloud_points_color_list.append(cloud_colors)
|
|
147
|
+
cloud_points_label_list.append(cloud_classes)
|
|
148
|
+
cloud_rooms_list.append(cloud_room_split)
|
|
149
|
+
|
|
150
|
+
sub_cloud_file = os.path.join(
|
|
151
|
+
processed_dir, cloud_name + f'_{voxel_size:.3f}_sub.pkl')
|
|
152
|
+
if os.path.exists(sub_cloud_file):
|
|
153
|
+
with open(sub_cloud_file, 'rb') as f:
|
|
154
|
+
sub_points, sub_colors, sub_labels, search_tree = pickle.load(
|
|
155
|
+
f)
|
|
156
|
+
else:
|
|
157
|
+
if voxel_size > 0:
|
|
158
|
+
sub_points, sub_colors, sub_labels = crop_pc(
|
|
159
|
+
cloud_points, cloud_colors, cloud_classes, voxel_size=voxel_size)
|
|
160
|
+
sub_labels = np.squeeze(sub_labels)
|
|
161
|
+
else:
|
|
162
|
+
sub_points = cloud_points
|
|
163
|
+
sub_colors = cloud_colors
|
|
164
|
+
sub_labels = cloud_classes
|
|
165
|
+
|
|
166
|
+
# Get chosen neighborhoods
|
|
167
|
+
search_tree = KDTree(sub_points, leaf_size=50)
|
|
168
|
+
|
|
169
|
+
with open(sub_cloud_file, 'wb') as f:
|
|
170
|
+
pickle.dump((sub_points, sub_colors,
|
|
171
|
+
sub_labels, search_tree), f)
|
|
172
|
+
|
|
173
|
+
sub_cloud_points_list.append(sub_points)
|
|
174
|
+
sub_cloud_points_color_list.append(sub_colors)
|
|
175
|
+
sub_cloud_points_label_list.append(sub_labels)
|
|
176
|
+
sub_cloud_tree_list.append(search_tree)
|
|
177
|
+
|
|
178
|
+
# original points
|
|
179
|
+
self.clouds_points = cloud_points_list
|
|
180
|
+
self.clouds_points_colors = cloud_points_color_list
|
|
181
|
+
self.clouds_points_labels = cloud_points_label_list
|
|
182
|
+
self.clouds_rooms = cloud_rooms_list
|
|
183
|
+
|
|
184
|
+
# grid subsampled points
|
|
185
|
+
self.sub_clouds_points = sub_cloud_points_list
|
|
186
|
+
self.sub_clouds_points_colors = sub_cloud_points_color_list
|
|
187
|
+
self.sub_clouds_points_labels = sub_cloud_points_label_list
|
|
188
|
+
self.sub_cloud_trees = sub_cloud_tree_list
|
|
189
|
+
|
|
190
|
+
with open(filename, 'wb') as f:
|
|
191
|
+
pickle.dump((self.clouds_points, self.clouds_points_colors, self.clouds_points_labels, self.clouds_rooms,
|
|
192
|
+
self.sub_clouds_points, self.sub_clouds_points_colors, self.sub_clouds_points_labels,
|
|
193
|
+
self.sub_cloud_trees), f)
|
|
194
|
+
print(f"{filename} saved successfully")
|
|
195
|
+
else:
|
|
196
|
+
with open(filename, 'rb') as f:
|
|
197
|
+
(self.clouds_points, self.clouds_points_colors, self.clouds_points_labels,
|
|
198
|
+
self.clouds_rooms,
|
|
199
|
+
self.sub_clouds_points, self.sub_clouds_points_colors, self.sub_clouds_points_labels,
|
|
200
|
+
self.sub_cloud_trees) = pickle.load(f)
|
|
201
|
+
print(f"{filename} loaded successfully")
|
|
202
|
+
|
|
203
|
+
# prepare iteration indices
|
|
204
|
+
filename = os.path.join(processed_dir,
|
|
205
|
+
f'{split}_{voxel_size:.3f}_{self.num_epochs}_{self.num_steps}_iterinds.pkl')
|
|
206
|
+
if not os.path.exists(filename):
|
|
207
|
+
potentials = []
|
|
208
|
+
min_potentials = [] # self.sub_cloud_trees, the tree for the grid_subsampled area
|
|
209
|
+
for cloud_i, tree in enumerate(self.sub_cloud_trees):
|
|
210
|
+
print(f"{split}/{cloud_i} has {tree.data.shape[0]} points")
|
|
211
|
+
cur_potential = np.random.rand(tree.data.shape[0]) * 1e-3
|
|
212
|
+
potentials.append(cur_potential)
|
|
213
|
+
min_potentials.append(float(np.min(cur_potential)))
|
|
214
|
+
self.cloud_inds = []
|
|
215
|
+
self.point_inds = []
|
|
216
|
+
self.noise = []
|
|
217
|
+
for ep in tqdm(range(self.num_epochs)):
|
|
218
|
+
for st in range(self.num_steps):
|
|
219
|
+
# cloud_ind is the index for area in each split. for test, only 1 cloud.
|
|
220
|
+
cloud_ind = int(np.argmin(min_potentials))
|
|
221
|
+
point_ind = np.argmin(potentials[cloud_ind])
|
|
222
|
+
self.cloud_inds.append(cloud_ind)
|
|
223
|
+
self.point_inds.append(point_ind)
|
|
224
|
+
points = np.array(
|
|
225
|
+
self.sub_cloud_trees[cloud_ind].data, copy=False) # sub_points
|
|
226
|
+
center_point = points[point_ind, :].reshape(1, -1)
|
|
227
|
+
noise = np.random.normal(
|
|
228
|
+
scale=self.in_radius / 10, size=center_point.shape)
|
|
229
|
+
self.noise.append(noise)
|
|
230
|
+
pick_point = center_point + noise.astype(center_point.dtype)
|
|
231
|
+
# Indices of points in input region
|
|
232
|
+
query_inds = self.sub_cloud_trees[cloud_ind].query_radius(pick_point,
|
|
233
|
+
r=self.in_radius,
|
|
234
|
+
return_distance=True,
|
|
235
|
+
sort_results=True)[0][0]
|
|
236
|
+
cur_num_points = query_inds.shape[0]
|
|
237
|
+
if self.num_points < cur_num_points:
|
|
238
|
+
query_inds = query_inds[:self.num_points]
|
|
239
|
+
# Update potentials (Tuckey weights)
|
|
240
|
+
dists = np.sum(
|
|
241
|
+
np.square((points[query_inds] - pick_point).astype(np.float32)), axis=1)
|
|
242
|
+
tukeys = np.square(1 - dists / np.square(self.in_radius))
|
|
243
|
+
tukeys[dists > np.square(self.in_radius)] = 0
|
|
244
|
+
potentials[cloud_ind][query_inds] += tukeys
|
|
245
|
+
min_potentials[cloud_ind] = float(
|
|
246
|
+
np.min(potentials[cloud_ind]))
|
|
247
|
+
with open(filename, 'wb') as f:
|
|
248
|
+
pickle.dump((self.cloud_inds, self.point_inds, self.noise), f)
|
|
249
|
+
print(f"{filename} saved successfully")
|
|
250
|
+
else:
|
|
251
|
+
with open(filename, 'rb') as f:
|
|
252
|
+
self.cloud_inds, self.point_inds, self.noise = pickle.load(f)
|
|
253
|
+
print(f"{filename} loaded successfully")
|
|
254
|
+
|
|
255
|
+
# prepare validation projection inds
|
|
256
|
+
filename = os.path.join(
|
|
257
|
+
processed_dir, f'{split}_{voxel_size:.3f}_proj.pkl')
|
|
258
|
+
if not os.path.exists(filename):
|
|
259
|
+
proj_ind_list = []
|
|
260
|
+
for points, search_tree in zip(self.clouds_points, self.sub_cloud_trees):
|
|
261
|
+
# points: the original points. 700K points
|
|
262
|
+
# subcloud_trees: the tree of the subsampeld points. 500K points
|
|
263
|
+
# find the nearest point in the subsampled points for each point in the original points.
|
|
264
|
+
proj_inds = np.squeeze(search_tree.query(
|
|
265
|
+
points, return_distance=False))
|
|
266
|
+
proj_inds = proj_inds.astype(np.int32)
|
|
267
|
+
proj_ind_list.append(proj_inds)
|
|
268
|
+
self.projections = proj_ind_list
|
|
269
|
+
with open(filename, 'wb') as f:
|
|
270
|
+
pickle.dump(self.projections, f)
|
|
271
|
+
print(f"{filename} saved successfully")
|
|
272
|
+
else:
|
|
273
|
+
with open(filename, 'rb') as f:
|
|
274
|
+
self.projections = pickle.load(f)
|
|
275
|
+
print(f"{filename} loaded successfully")
|
|
276
|
+
|
|
277
|
+
def __getitem__(self, idx):
|
|
278
|
+
"""
|
|
279
|
+
Returns:
|
|
280
|
+
pts: (N, 3), a point cloud.
|
|
281
|
+
mask: (N, ), 0/1 mask to distinguish padding points.
|
|
282
|
+
features: (input_features_dim, N), input points features.
|
|
283
|
+
pts_labels: (N), point label.
|
|
284
|
+
current_cloud_index: (1), cloud index.
|
|
285
|
+
input_inds: (N), the index of input points in point cloud.
|
|
286
|
+
"""
|
|
287
|
+
cloud_ind = self.cloud_inds[idx + self.epoch * self.num_steps]
|
|
288
|
+
point_ind = self.point_inds[idx + self.epoch * self.num_steps]
|
|
289
|
+
noise = self.noise[idx + self.epoch * self.num_steps]
|
|
290
|
+
points = np.array(
|
|
291
|
+
self.sub_cloud_trees[cloud_ind].data, copy=False) # subpoints
|
|
292
|
+
center_point = points[point_ind, :].reshape(1, -1)
|
|
293
|
+
pick_point = center_point + noise.astype(center_point.dtype)
|
|
294
|
+
# Indices of points in input region
|
|
295
|
+
# the point index in the subsampled point tree.
|
|
296
|
+
query_inds = self.sub_cloud_trees[cloud_ind].query_radius(pick_point,
|
|
297
|
+
r=self.in_radius,
|
|
298
|
+
return_distance=True,
|
|
299
|
+
sort_results=True)[0][0]
|
|
300
|
+
# Number collected
|
|
301
|
+
cur_num_points = query_inds.shape[0]
|
|
302
|
+
if self.num_points < cur_num_points:
|
|
303
|
+
shuffle_choice = np.random.permutation(np.arange(self.num_points))
|
|
304
|
+
input_inds = query_inds[:self.num_points][shuffle_choice]
|
|
305
|
+
mask = torch.ones(self.num_points).type(torch.int32)
|
|
306
|
+
else:
|
|
307
|
+
shuffle_choice = np.random.permutation(np.arange(cur_num_points))
|
|
308
|
+
query_inds = query_inds[shuffle_choice]
|
|
309
|
+
padding_choice = np.random.choice(
|
|
310
|
+
cur_num_points, self.num_points - cur_num_points)
|
|
311
|
+
input_inds = np.hstack([query_inds, query_inds[padding_choice]])
|
|
312
|
+
mask = torch.zeros(self.num_points).type(torch.int32)
|
|
313
|
+
mask[:cur_num_points] = 1
|
|
314
|
+
# points: the whole point of an area (voxel downsampled).
|
|
315
|
+
# input_inds: the point index of the current area.
|
|
316
|
+
original_points = points[input_inds]
|
|
317
|
+
pts = (original_points - pick_point).astype(np.float32)
|
|
318
|
+
colors = self.sub_clouds_points_colors[cloud_ind][input_inds].astype(
|
|
319
|
+
np.float32)
|
|
320
|
+
labels = self.sub_clouds_points_labels[cloud_ind][input_inds].astype(
|
|
321
|
+
np.int64)
|
|
322
|
+
current_cloud_index = np.array(cloud_ind).astype(np.int64)
|
|
323
|
+
|
|
324
|
+
data = {'pos': pts,
|
|
325
|
+
'x': colors,
|
|
326
|
+
'y': labels,
|
|
327
|
+
'mask': mask,
|
|
328
|
+
'cloud_index': current_cloud_index,
|
|
329
|
+
'input_inds': input_inds,
|
|
330
|
+
}
|
|
331
|
+
""" vis
|
|
332
|
+
from openpoints.dataset import vis_multi_points, vis_points
|
|
333
|
+
vis_multi_points([points, pts], [self.sub_clouds_points_colors[cloud_ind]/255., colors/255.])
|
|
334
|
+
|
|
335
|
+
import copy
|
|
336
|
+
old_data = copy.deepcopy(data)
|
|
337
|
+
if self.transform is not None:
|
|
338
|
+
data = self.transform(data)
|
|
339
|
+
vis_multi_points([old_data['pos'][:, :3], data['pos'][:, :3].numpy()], colors=[old_data['x'][:, :3]/255.,data['x'][:, :3].numpy()])
|
|
340
|
+
"""
|
|
341
|
+
if self.transform is not None:
|
|
342
|
+
data = self.transform(data)
|
|
343
|
+
|
|
344
|
+
if 'heights' not in data.keys():
|
|
345
|
+
data['heights'] = torch.from_numpy(original_points[:, self.gravity_dim:self.gravity_dim+1].astype(np.float32))
|
|
346
|
+
return data
|
|
347
|
+
|
|
348
|
+
def __len__(self):
|
|
349
|
+
return self.num_steps
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .scannet import ScanNet
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import os.path as osp
|
|
3
|
+
import numpy as np
|
|
4
|
+
import torch
|
|
5
|
+
from torch.utils.data import Dataset
|
|
6
|
+
from ..build import DATASETS
|
|
7
|
+
from ..data_util import crop_pc, voxelize
|
|
8
|
+
from ...transforms.point_transform_cpu import PointsToTensor
|
|
9
|
+
import glob
|
|
10
|
+
from tqdm import tqdm
|
|
11
|
+
import logging
|
|
12
|
+
import pickle
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
VALID_CLASS_IDS = [
|
|
16
|
+
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 16, 24, 28, 33, 34, 36, 39
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
SCANNET_COLOR_MAP = {
|
|
20
|
+
0: (0., 0., 0.),
|
|
21
|
+
1: (174., 199., 232.),
|
|
22
|
+
2: (152., 223., 138.),
|
|
23
|
+
3: (31., 119., 180.),
|
|
24
|
+
4: (255., 187., 120.),
|
|
25
|
+
5: (188., 189., 34.),
|
|
26
|
+
6: (140., 86., 75.),
|
|
27
|
+
7: (255., 152., 150.),
|
|
28
|
+
8: (214., 39., 40.),
|
|
29
|
+
9: (197., 176., 213.),
|
|
30
|
+
10: (148., 103., 189.),
|
|
31
|
+
11: (196., 156., 148.),
|
|
32
|
+
12: (23., 190., 207.),
|
|
33
|
+
14: (247., 182., 210.),
|
|
34
|
+
15: (66., 188., 102.),
|
|
35
|
+
16: (219., 219., 141.),
|
|
36
|
+
17: (140., 57., 197.),
|
|
37
|
+
18: (202., 185., 52.),
|
|
38
|
+
19: (51., 176., 203.),
|
|
39
|
+
20: (200., 54., 131.),
|
|
40
|
+
21: (92., 193., 61.),
|
|
41
|
+
22: (78., 71., 183.),
|
|
42
|
+
23: (172., 114., 82.),
|
|
43
|
+
24: (255., 127., 14.),
|
|
44
|
+
25: (91., 163., 138.),
|
|
45
|
+
26: (153., 98., 156.),
|
|
46
|
+
27: (140., 153., 101.),
|
|
47
|
+
28: (158., 218., 229.),
|
|
48
|
+
29: (100., 125., 154.),
|
|
49
|
+
30: (178., 127., 135.),
|
|
50
|
+
32: (146., 111., 194.),
|
|
51
|
+
33: (44., 160., 44.),
|
|
52
|
+
34: (112., 128., 144.),
|
|
53
|
+
35: (96., 207., 209.),
|
|
54
|
+
36: (227., 119., 194.),
|
|
55
|
+
37: (213., 92., 176.),
|
|
56
|
+
38: (94., 106., 211.),
|
|
57
|
+
39: (82., 84., 163.),
|
|
58
|
+
40: (100., 85., 144.),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@DATASETS.register_module()
|
|
64
|
+
class ScanNet(Dataset):
|
|
65
|
+
num_classes = 20
|
|
66
|
+
classes = ['wall', 'floor', 'cabinet', 'bed', 'chair', 'sofa', 'table', 'door', 'window', 'bookshelf', 'picture',
|
|
67
|
+
'counter', 'desk', 'curtain', 'refrigerator', 'shower curtain', 'toilet', 'sink', 'bathtub', 'otherfurniture']
|
|
68
|
+
gravity_dim = 2
|
|
69
|
+
|
|
70
|
+
color_mean = [0.46259782, 0.46253258, 0.46253258]
|
|
71
|
+
color_std = [0.693565 , 0.6852543 , 0.68061745]
|
|
72
|
+
"""ScanNet dataset, loading the subsampled entire room as input without block/sphere subsampling.
|
|
73
|
+
number of points per room in average, median, and std: (145841.0, 158783.87179487178, 84200.84445829492)
|
|
74
|
+
"""
|
|
75
|
+
def __init__(self,
|
|
76
|
+
data_root='data/ScanNet',
|
|
77
|
+
split='train',
|
|
78
|
+
voxel_size=0.04,
|
|
79
|
+
voxel_max=None,
|
|
80
|
+
transform=None,
|
|
81
|
+
loop=1, presample=False, variable=False,
|
|
82
|
+
n_shifted=1
|
|
83
|
+
):
|
|
84
|
+
super().__init__()
|
|
85
|
+
self.split = split
|
|
86
|
+
self.voxel_size = voxel_size
|
|
87
|
+
self.voxel_max = voxel_max
|
|
88
|
+
self.transform = transform
|
|
89
|
+
self.presample = presample
|
|
90
|
+
self.variable = variable
|
|
91
|
+
self.loop = loop
|
|
92
|
+
self.n_shifted = n_shifted
|
|
93
|
+
self.pipe_transform = PointsToTensor()
|
|
94
|
+
|
|
95
|
+
if split == "train" or split == 'val':
|
|
96
|
+
self.data_list = glob.glob(os.path.join(data_root, split, "*.pth"))
|
|
97
|
+
elif split == 'trainval':
|
|
98
|
+
self.data_list = glob.glob(os.path.join(
|
|
99
|
+
data_root, "train", "*.pth")) + glob.glob(os.path.join(data_root, "val", "*.pth"))
|
|
100
|
+
elif split == 'test':
|
|
101
|
+
self.data_list = glob.glob(os.path.join(data_root, split, "*.pth"))
|
|
102
|
+
else:
|
|
103
|
+
raise ValueError("no such split: {}".format(split))
|
|
104
|
+
|
|
105
|
+
logging.info("Totally {} samples in {} set.".format(
|
|
106
|
+
len(self.data_list), split))
|
|
107
|
+
|
|
108
|
+
processed_root = os.path.join(data_root, 'processed')
|
|
109
|
+
filename = os.path.join(
|
|
110
|
+
processed_root, f'scannet_{split}_{voxel_size:.3f}.pkl')
|
|
111
|
+
if presample and not os.path.exists(filename):
|
|
112
|
+
np.random.seed(0)
|
|
113
|
+
self.data = []
|
|
114
|
+
for item in tqdm(self.data_list, desc=f'Loading ScanNet {split} split'):
|
|
115
|
+
data = torch.load(item)
|
|
116
|
+
coord, feat, label = data[0:3]
|
|
117
|
+
coord, feat, label = crop_pc(
|
|
118
|
+
coord, feat, label, self.split, self.voxel_size, self.voxel_max, variable=self.variable)
|
|
119
|
+
cdata = np.hstack(
|
|
120
|
+
(coord, feat, np.expand_dims(label, -1))).astype(np.float32)
|
|
121
|
+
self.data.append(cdata)
|
|
122
|
+
npoints = np.array([len(data) for data in self.data])
|
|
123
|
+
logging.info('split: %s, median npoints %.1f, avg num points %.1f, std %.1f' % (
|
|
124
|
+
self.split, np.median(npoints), np.average(npoints), np.std(npoints)))
|
|
125
|
+
os.makedirs(processed_root, exist_ok=True)
|
|
126
|
+
with open(filename, 'wb') as f:
|
|
127
|
+
pickle.dump(self.data, f)
|
|
128
|
+
print(f"{filename} saved successfully")
|
|
129
|
+
elif presample:
|
|
130
|
+
with open(filename, 'rb') as f:
|
|
131
|
+
self.data = pickle.load(f)
|
|
132
|
+
print(f"{filename} load successfully")
|
|
133
|
+
# median, average, std of number of points after voxel sampling for val set.
|
|
134
|
+
# (100338.5, 109686.1282051282, 57024.51083415437)
|
|
135
|
+
# before voxel sampling
|
|
136
|
+
# (145841.0, 158783.87179487178, 84200.84445829492)
|
|
137
|
+
def __getitem__(self, idx):
|
|
138
|
+
data_idx = idx % len(self.data_list)
|
|
139
|
+
if self.presample:
|
|
140
|
+
coord, feat, label = np.split(self.data[data_idx], [3, 6], axis=1)
|
|
141
|
+
else:
|
|
142
|
+
data_path = self.data_list[data_idx]
|
|
143
|
+
data = torch.load(data_path)
|
|
144
|
+
coord, feat, label = data[0:3]
|
|
145
|
+
|
|
146
|
+
feat = (feat + 1) * 127.5
|
|
147
|
+
label = label.astype(np.long).squeeze()
|
|
148
|
+
data = {'pos': coord.astype(np.float32), 'x': feat.astype(np.float32), 'y': label}
|
|
149
|
+
"""debug
|
|
150
|
+
from openpoints.dataset import vis_multi_points
|
|
151
|
+
import copy
|
|
152
|
+
old_data = copy.deepcopy(data)
|
|
153
|
+
if self.transform is not None:
|
|
154
|
+
data = self.transform(data)
|
|
155
|
+
data['pos'], data['x'], data['y'] = crop_pc(
|
|
156
|
+
data['pos'], data['x'], data['y'], self.split, self.voxel_size, self.voxel_max,
|
|
157
|
+
downsample=not self.presample, variable=self.variable)
|
|
158
|
+
|
|
159
|
+
vis_multi_points([old_data['pos'][:, :3], data['pos'][:, :3]], colors=[old_data['x'][:, :3]/255.,data['x'][:, :3]])
|
|
160
|
+
"""
|
|
161
|
+
if self.transform is not None:
|
|
162
|
+
data = self.transform(data)
|
|
163
|
+
|
|
164
|
+
if not self.presample:
|
|
165
|
+
data['pos'], data['x'], data['y'] = crop_pc(
|
|
166
|
+
data['pos'], data['x'], data['y'], self.split, self.voxel_size, self.voxel_max,
|
|
167
|
+
downsample=not self.presample, variable=self.variable)
|
|
168
|
+
|
|
169
|
+
data = self.pipe_transform(data)
|
|
170
|
+
|
|
171
|
+
if 'heights' not in data.keys():
|
|
172
|
+
data['heights'] = data['pos'][:, self.gravity_dim:self.gravity_dim+1] - data['pos'][:, self.gravity_dim:self.gravity_dim+1].min()
|
|
173
|
+
return data
|
|
174
|
+
|
|
175
|
+
def __len__(self):
|
|
176
|
+
return len(self.data_list) * self.loop
|