探秘S3DIS数据集
本博文先介绍S3DIS的路径结构,从而对该数据集有一个整体的了解,然后会在第二节中了解一下pointnet++中如何对该数据集转换,以及转换的原因,同时在第三节中会介绍pointnet++中如何构建语义分割数据集,最后会接受如何对S3DIS数据集进行预测。
1.数据路径结构
以PointNet++中的S3DIS为例,s3dis/Stanford3dDataset_v1.2_Aligned_Version路径下有6个区域,如下所示:

每一个Area下面是一些区域,比如Area_1下有两个会议室(ConferenceRoom),八个走廊(hallway),若干个办公室(office)等。

将会议室1展开,发现有一个Annotations文件夹,一个conferenceRoom_1.txt文件,其中Annotations里保存的是会议室1中所有物品的xyzrgb坐标,均为n行6列的数据,而文件名则表示该物品的标签。比如chair_1.txt中共6729行6列,表示该物品由6729个点组成,且包含xyzrgb六个参数,标签为chair,可视化结果如下所示。

下图为Annotations展开后的路径结构。

下图为chair_1.txt里的内容以及使用CloudCompare可视化的结果


而另外一个ConferenceRoom_1.txt保存的是这个会议室中所有的点,有1136677个点,可见一个会议室的房间的点都这么多,那么一个Area的点不得上千万呀,可视化如下所示,当然,放大去看也能找到刚刚可视化的那张椅子。


2.collect_indoor3d_data数据转换
在pointnet++中,往往需要对原始的S3DIS进行转换,那么为什么需要转换?因为S3DIS数据集只是存储一些点,并没有标签(标签是存储在文件名上的),而collect_indoor3d_data脚本所做的事情就是将每一个Area下的每一个场景的点和标签进行合并,并且保存为.npy格式,加速读取的速度。转换后的数据集如下所示,也可以参考我的另一篇博文如何使用S3DIS训练pointent++语义分割模型,Win10系统下复现Pointnet++(pytorch)_吃鱼不卡次的博客-CSDN博客:

可以使用numpy.load()打开,查看其形状,可以看到一共有七列,最后一列表示的是类别,如下所示:

3.构建数据集S3DISDataset
训练的时候使用的S3DISDataset来构建数据集(如下所示),测试的时候使ScannetDatasetWholeScene来构造数据集,两者是有区别的,测试的会放在下一节来介绍。
from data_utils.S3DISDataLoader import S3DISDatasetprint("start loading training data ...")TRAIN_DATASET = S3DISDataset(split='train', data_root=root, num_point=NUM_POINT, test_area=args.test_area, block_size=1.0, sample_rate=1.0, transform=None)print("start loading test data ...")TEST_DATASET = S3DISDataset(split='test', data_root=root, num_point=NUM_POINT, test_area=args.test_area, block_size=1.0, sample_rate=1.0, transform=None)
众所周知,dataset一定要包括三个函数,初始化函数__init_(),依次返回数据的函数__getitem__(),获取数据长度的函数__len__(),下面我以下面这4个.npy文件作为例子,分别来介绍一下这三个函数。

3.1__init__()函数
首先,先看一下函数的参数:
split表示构建数据集的类型,有train和test两种,分别表示训练集和测试集;如果是测试集,那么会使用test_area参数,比如test_area=5这个参数将指定Area5作为测试集,第一节已经介绍过了,一共六个区域,每个区域的点的数量很庞大,选择其中一个区域作为测试集(验证集)也是合理的,而且也可以当成是5:1划分数据集了。
data_root表示数据的路径,调用的就是第二节中转换而来得到的全是.npy文件的路径,即s3dis\Stanford3dDataset_v1.2_Aligned_Version。num_point表示经过预处理后输入网络的点的数量,比如默认设置为4096,则表示输入网络的点数为4096。sample_rate为原图下采样的倍数,dataset处理流程是这样的,先对原图进行下采样,再对下采样后的点云进行随机选取区域(每个区域的大小为4096)。block_size为随机选择区域的宽高。
一口气说完那么多,是否觉得很迷糊,没关系,后面都会详细去讲的。
其次,看一下后面的代码:
rooms比较简单,存储的是npy的文件名,该例子中为['Area_1_conferenceRoom_1.npy', 'Area_1_conferenceRoom_2.npy', 'Area_1_copyRoom_1.npy', 'Area_5_office_12.npy'];
rooms_split为划分训练集和验证集,如果该数据集为训练集,将把包含Area_5的数据排除在外,则rooms_split=['Area_1_conferenceRoom_1.npy', 'Area_1_conferenceRoom_2.npy', 'Area_1_copyRoom_1.npy'];如果该数据集为测试集,则rooms_split=['Area_5_office_12.npy']。
def __init__(self, split='train', data_root='trainval_fullarea', num_point=4096, test_area=5, block_size=1.0, sample_rate=1.0, transform=None):super().__init__()self.num_point = num_pointself.block_size = block_sizeself.transform = transformrooms = sorted(os.listdir(data_root))rooms = [room for room in rooms if 'Area_' in room]if split == 'train':rooms_split = [room for room in rooms if not 'Area_{}'.format(test_area) in room]else:rooms_split = [room for room in rooms if 'Area_{}'.format(test_area) in room]
这几个参数主要是保存各项参数的,后面再说。
self.room_points, self.room_labels = [], []self.room_coord_min, self.room_coord_max = [], []num_point_all = []labelweights = np.zeros(13)
再次,这段代码是逐个读取rooms_split中的数据,通过points保存每个文件的xyzrgb共n行6列信息,labels保存对应点的标签信息(n行1列);通过np.histogram()来统计每个类别出现的次数,以rooms_split[0]为例子,返回的tmp为array([213074, 190384, 354422, 61528, 0, 0, 41345, 31049,77761, 0, 20437, 86833, 59784], dtype=int64),代表着rooms_split[0]的这个点云,统计的13个类别的数量,比如,第一个类别出现了213074个,这些所有值相加就是这个点云的点的数量,那么为什么要统计这个呢,是为了后面做损失的时候设置一个类别权重,遍历完所有数据之后labelweights += tmp 中的labelweights就会统计完所有数据的所有类别数量。
coord_min, coord_max分别代表的是xyz中最小的点以及最大的点,举个例子,如下所示:points为一个3行3列的数组,np.amin()得到的值[1,2,-1]是每一列中的最小值,然后说一下对于axis参数的理解:axis=0是指第零个维度发生变化,即由3行3列变为1行3列,那么也就是说在每一列中找最小值。

self.room_points是一个列表,列表中存储的是每个点云的点,列表的大小即为点云的数量。self.room_labels也是一个列表,存储的是self.room_points中每个点云下的点的类别。同理,self.room_coord_min和self.room_coord_max也是列表,存储的是每个点云的xyz的最小值以及最大值。num_point_all存储的是每个点云的数量。
遍历完所有点云后可以看到,self.room_points(如下图所示)列表的长度为3,存储的是3个点云的xyzrgb信息,其中第3个点云的形状为(510949,6)表示的是该点云有510949个点,每个点有6个属性。self.room_labels每个元素的shape为(510949,1),self.room_coord_min和self.room_coord_max每个元素的shape为(1,3),num_point_all每个元素的shape为(1,)。

for room_name in tqdm(rooms_split, total=len(rooms_split)):room_path = os.path.join(data_root, room_name)room_data = np.load(room_path) # xyzrgbl, N*7points, labels = room_data[:, 0:6], room_data[:, 6] # xyzrgb, N*6; l, Ntmp, _ = np.histogram(labels, range(14))labelweights += tmpcoord_min, coord_max = np.amin(points, axis=0)[:3], np.amax(points, axis=0)[:3]self.room_points.append(points), self.room_labels.append(labels)self.room_coord_min.append(coord_min), self.room_coord_max.append(coord_max)num_point_all.append(labels.size)
接着,下面是对类别权重的处理,遍历完所有点云后,labelweights存储的是各个类别的点的数量,设置权重一般是把点数多的类别权重设置小一点,因为点数多说明这个类别相较于点数少的类别更容易学习,那么最简单的方法就是取点数的倒数,但是这个过于简单粗暴,那么我们看看pointnet++是如何设置类别权重的(这段我直接问ChatGPT了,解释得很清楚,也能理解):
1.labelweights = labelweights.astype(np.float32):将 labelweights 数组的数据类型转换为 np.float32,以便后续的数值计算。
2.labelweights = labelweights / np.sum(labelweights):将 labelweights 数组中的每个元素除以数组中所有元素的和,以获得每个类别的相对权重。这样做可以确保权重的总和为 1。
3.np.power(np.amax(labelweights) / labelweights, 1 / 3.0):计算每个类别权重相对于最大权重的比例的三分之一次方。这个操作的目的是将较大的权重值进行缩放,以确保它们不会在损失计算中产生过大的影响。这有助于平衡损失在各个类别之间的影响。
labelweights = labelweights.astype(np.float32)labelweights = labelweights / np.sum(labelweights)self.labelweights = np.power(np.amax(labelweights) / labelweights, 1 / 3.0)print(self.labelweights)
最后,最重要的一个参数登场了,room_idxs,这段代码其实就是分配一下每一个点云要采多少个区域输入到网络里面去训练。
sample_prob的值为array([0.35713406, 0.48232172, 0.16054422]),代表这每个点云中点的数量占总点数的比例,然后会按照这个比例来进行分配。
num_iter其实就是粗略计算了一下总共要采多少个区域,np.sum(num_point_all) * sample_rate可以理解为点云下采样后还剩多少点,然后再除以num_point,意思是剩下的点可以被分成多少块区域,每个区域num_point个点,其实我觉得完全可以设定一个参数,就训练1000张图片好像也不是不行。
room_idxs中保存的是每个点云要采样的次数,举个例子说明一下(如下所示):遍历完所有点云之后,共得到27个0,表示第一个点云我要随机裁剪27个区域,第二个点云要随机裁剪成37个区域,第三个点云同理,那么训练集一共有76个数据送去训练。那么27,37还有12是怎么得到的呢,是通过int(round(sample_prob[index] * num_iter))得到的,其实也就是按照点云的点数占总点数的比例来得到每个点云所要选择的区域的。

sample_prob = num_point_all / np.sum(num_point_all)num_iter = int(np.sum(num_point_all) * sample_rate / num_point)room_idxs = []for index in range(len(rooms_split)):room_idxs.extend([index] * int(round(sample_prob[index] * num_iter)))self.room_idxs = np.array(room_idxs)print("Totally {} samples in {} set.".format(len(self.room_idxs), split))
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
