# # # 0=================================0 # | Kernel Point Convolutions | # 0=================================0 # # # ---------------------------------------------------------------------------------------------------------------------- # # Class handling S3DIS dataset. # Implements a Dataset, a Sampler, and a collate_fn # # ---------------------------------------------------------------------------------------------------------------------- # # Hugues THOMAS - 11/06/2018 # # ---------------------------------------------------------------------------------------------------------------------- # # Imports and global variables # \**********************************/ # # Common libs import time import numpy as np import pickle import torch import warnings from multiprocessing import Lock # OS functions from os import listdir from os.path import exists, join, isdir # Dataset parent class from datasetss.common import PointCloudDataset from torch.utils.data import Sampler, get_worker_info from utils.mayavi_visu import * from datasetss.common import grid_subsampling from utils.config import bcolors # ---------------------------------------------------------------------------------------------------------------------- # # Dataset class definition # \******************************/ class S3DISDataset(PointCloudDataset): """Class to handle S3DIS dataset.""" def __init__(self, config, set="training", use_potentials=True, load_data=True): """ This dataset is small enough to be stored in-memory, so load all point clouds here """ PointCloudDataset.__init__(self, "S3DIS") ############ # Parameters ############ # Dict from labels to names self.label_to_names = { 0: "ceiling", 1: "floor", 2: "wall", 3: "beam", 4: "column", 5: "window", 6: "door", 7: "chair", 8: "table", 9: "bookcase", 10: "sofa", 11: "board", 12: "clutter", } # Initialize a bunch of variables concerning class labels self.init_labels() # List of classes ignored during training (can be empty) self.ignored_labels = np.array([]) # Dataset folder self.path = "./Data/S3DIS" # Type of task conducted on this dataset self.dataset_task = "cloud_segmentation" # Update number of class and data task in configuration config.num_classes = self.num_classes - len(self.ignored_labels) config.dataset_task = self.dataset_task # Parameters from config self.config = config # Training or test set self.set = set # Using potential or random epoch generation self.use_potentials = use_potentials # Path of the training files self.train_path = "original_ply" # List of files to process ply_path = join(self.path, self.train_path) # Proportion of validation scenes self.cloud_names = ["Area_1", "Area_2", "Area_3", "Area_4", "Area_5", "Area_6"] self.all_splits = [0, 1, 2, 3, 4, 5] self.validation_split = 4 # Number of models used per epoch if self.set == "training": self.epoch_n = config.epoch_steps * config.batch_num elif self.set in ["validation", "test", "ERF"]: self.epoch_n = config.validation_size * config.batch_num else: raise ValueError("Unknown set for S3DIS data: ", self.set) # Stop data is not needed if not load_data: return ################### # Prepare ply files ################### self.prepare_S3DIS_ply() ################ # Load ply files ################ # List of training files self.files = [] for i, f in enumerate(self.cloud_names): if self.set == "training": if self.all_splits[i] != self.validation_split: self.files += [join(ply_path, f + ".ply")] elif self.set in ["validation", "test", "ERF"]: if self.all_splits[i] == self.validation_split: self.files += [join(ply_path, f + ".ply")] else: raise ValueError("Unknown set for S3DIS data: ", self.set) if self.set == "training": self.cloud_names = [ f for i, f in enumerate(self.cloud_names) if self.all_splits[i] != self.validation_split ] elif self.set in ["validation", "test", "ERF"]: self.cloud_names = [ f for i, f in enumerate(self.cloud_names) if self.all_splits[i] == self.validation_split ] if 0 < self.config.first_subsampling_dl <= 0.01: raise ValueError("subsampling_parameter too low (should be over 1 cm") # Initiate containers self.input_trees = [] self.input_colors = [] self.input_labels = [] self.pot_trees = [] self.num_clouds = 0 self.test_proj = [] self.validation_labels = [] # Start loading self.load_subsampled_clouds() ############################ # Batch selection parameters ############################ # Initialize value for batch limit (max number of points per batch). self.batch_limit = torch.tensor([1], dtype=torch.float32) self.batch_limit.share_memory_() # Initialize potentials if use_potentials: self.potentials = [] self.min_potentials = [] self.argmin_potentials = [] for i, tree in enumerate(self.pot_trees): self.potentials += [ torch.from_numpy(np.random.rand(tree.data.shape[0]) * 1e-3) ] min_ind = int(torch.argmin(self.potentials[-1])) self.argmin_potentials += [min_ind] self.min_potentials += [float(self.potentials[-1][min_ind])] # Share potential memory self.argmin_potentials = torch.from_numpy( np.array(self.argmin_potentials, dtype=np.int64) ) self.min_potentials = torch.from_numpy( np.array(self.min_potentials, dtype=np.float64) ) self.argmin_potentials.share_memory_() self.min_potentials.share_memory_() for i, _ in enumerate(self.pot_trees): self.potentials[i].share_memory_() self.worker_waiting = torch.tensor( [0 for _ in range(config.input_threads)], dtype=torch.int32 ) self.worker_waiting.share_memory_() self.epoch_inds = None self.epoch_i = 0 else: self.potentials = None self.min_potentials = None self.argmin_potentials = None self.epoch_inds = torch.from_numpy( np.zeros((2, self.epoch_n), dtype=np.int64) ) self.epoch_i = torch.from_numpy(np.zeros((1,), dtype=np.int64)) self.epoch_i.share_memory_() self.epoch_inds.share_memory_() self.worker_lock = Lock() # For ERF visualization, we want only one cloud per batch and no randomness if self.set == "ERF": self.batch_limit = torch.tensor([1], dtype=torch.float32) self.batch_limit.share_memory_() np.random.seed(42) return def __len__(self): """ Return the length of data here """ return len(self.cloud_names) def __getitem__(self, batch_i): """ The main thread gives a list of indices to load a batch. Each worker is going to work in parallel to load a different list of indices. """ if self.use_potentials: return self.potential_item(batch_i) else: return self.random_item(batch_i) def potential_item(self, batch_i, debug_workers=False): t = [time.time()] # Initiate concatanation lists p_list = [] f_list = [] l_list = [] i_list = [] pi_list = [] ci_list = [] s_list = [] R_list = [] batch_n = 0 failed_attempts = 0 info = get_worker_info() if info is not None: wid = info.id else: wid = None while True: t += [time.time()] if debug_workers: message = "" for wi in range(info.num_workers): if wi == wid: message += " {:}X{:} ".format(bcolors.FAIL, bcolors.ENDC) elif self.worker_waiting[wi] == 0: message += " " elif self.worker_waiting[wi] == 1: message += " | " elif self.worker_waiting[wi] == 2: message += " o " print(message) self.worker_waiting[wid] = 0 with self.worker_lock: if debug_workers: message = "" for wi in range(info.num_workers): if wi == wid: message += " {:}v{:} ".format(bcolors.OKGREEN, bcolors.ENDC) elif self.worker_waiting[wi] == 0: message += " " elif self.worker_waiting[wi] == 1: message += " | " elif self.worker_waiting[wi] == 2: message += " o " print(message) self.worker_waiting[wid] = 1 # Get potential minimum cloud_ind = int(torch.argmin(self.min_potentials)) point_ind = int(self.argmin_potentials[cloud_ind]) # Get potential points from tree structure pot_points = np.array(self.pot_trees[cloud_ind].data, copy=False) # Center point of input region center_point = pot_points[point_ind, :].reshape(1, -1) # Add a small noise to center point if self.set != "ERF": center_point += np.random.normal( scale=self.config.in_radius / 10, size=center_point.shape ) # Indices of points in input region pot_inds, dists = self.pot_trees[cloud_ind].query_radius( center_point, r=self.config.in_radius, return_distance=True ) d2s = np.square(dists[0]) pot_inds = pot_inds[0] # Update potentials (Tukey weights) if self.set != "ERF": tukeys = np.square(1 - d2s / np.square(self.config.in_radius)) tukeys[d2s > np.square(self.config.in_radius)] = 0 self.potentials[cloud_ind][pot_inds] += tukeys min_ind = torch.argmin(self.potentials[cloud_ind]) self.min_potentials[[cloud_ind]] = self.potentials[cloud_ind][ min_ind ] self.argmin_potentials[[cloud_ind]] = min_ind t += [time.time()] # Get points from tree structure points = np.array(self.input_trees[cloud_ind].data, copy=False) # Indices of points in input region input_inds = self.input_trees[cloud_ind].query_radius( center_point, r=self.config.in_radius )[0] t += [time.time()] # Number collected n = input_inds.shape[0] # Safe check for empty spheres if n < 2: failed_attempts += 1 if failed_attempts > 100 * self.config.batch_num: raise ValueError( "It seems this dataset only containes empty input spheres" ) t += [time.time()] t += [time.time()] continue # Collect labels and colors input_points = (points[input_inds] - center_point).astype(np.float32) input_colors = self.input_colors[cloud_ind][input_inds] if self.set in ["test", "ERF"]: input_labels = np.zeros(input_points.shape[0]) else: input_labels = self.input_labels[cloud_ind][input_inds] input_labels = np.array([self.label_to_idx[l] for l in input_labels]) t += [time.time()] # Data augmentation input_points, scale, R = self.augmentation_transform(input_points) # Color augmentation if np.random.rand() > self.config.augment_color: input_colors *= 0 # Get original height as additional feature input_features = np.hstack( (input_colors, input_points[:, 2:] + center_point[:, 2:]) ).astype(np.float32) t += [time.time()] # Stack batch p_list += [input_points] f_list += [input_features] l_list += [input_labels] pi_list += [input_inds] i_list += [point_ind] ci_list += [cloud_ind] s_list += [scale] R_list += [R] # Update batch size batch_n += n # In case batch is full, stop if batch_n > int(self.batch_limit): break # Randomly drop some points (act as an augmentation process and a safety for GPU memory consumption) # if n > int(self.batch_limit): # input_inds = np.random.choice(input_inds, size=int(self.batch_limit) - 1, replace=False) # n = input_inds.shape[0] ################### # Concatenate batch ################### stacked_points = np.concatenate(p_list, axis=0) features = np.concatenate(f_list, axis=0) labels = np.concatenate(l_list, axis=0) point_inds = np.array(i_list, dtype=np.int32) cloud_inds = np.array(ci_list, dtype=np.int32) input_inds = np.concatenate(pi_list, axis=0) stack_lengths = np.array([pp.shape[0] for pp in p_list], dtype=np.int32) scales = np.array(s_list, dtype=np.float32) rots = np.stack(R_list, axis=0) # Input features stacked_features = np.ones_like(stacked_points[:, :1], dtype=np.float32) if self.config.in_features_dim == 1: pass elif self.config.in_features_dim == 4: stacked_features = np.hstack((stacked_features, features[:, :3])) elif self.config.in_features_dim == 5: stacked_features = np.hstack((stacked_features, features)) else: raise ValueError( "Only accepted input dimensions are 1, 4 and 7 (without and with XYZ)" ) ####################### # Create network inputs ####################### # # Points, neighbors, pooling indices for each layers # t += [time.time()] # Get the whole input list input_list = self.segmentation_inputs( stacked_points, stacked_features, labels, stack_lengths ) t += [time.time()] # Add scale and rotation for testing input_list += [scales, rots, cloud_inds, point_inds, input_inds] if debug_workers: message = "" for wi in range(info.num_workers): if wi == wid: message += " {:}0{:} ".format(bcolors.OKBLUE, bcolors.ENDC) elif self.worker_waiting[wi] == 0: message += " " elif self.worker_waiting[wi] == 1: message += " | " elif self.worker_waiting[wi] == 2: message += " o " print(message) self.worker_waiting[wid] = 2 t += [time.time()] # Display timings debugT = False if debugT: print("\n************************\n") print("Timings:") ti = 0 N = 5 mess = "Init ...... {:5.1f}ms /" loop_times = [ 1000 * (t[ti + N * i + 1] - t[ti + N * i]) for i in range(len(stack_lengths)) ] for dt in loop_times: mess += " {:5.1f}".format(dt) print(mess.format(np.sum(loop_times))) ti += 1 mess = "Pots ...... {:5.1f}ms /" loop_times = [ 1000 * (t[ti + N * i + 1] - t[ti + N * i]) for i in range(len(stack_lengths)) ] for dt in loop_times: mess += " {:5.1f}".format(dt) print(mess.format(np.sum(loop_times))) ti += 1 mess = "Sphere .... {:5.1f}ms /" loop_times = [ 1000 * (t[ti + N * i + 1] - t[ti + N * i]) for i in range(len(stack_lengths)) ] for dt in loop_times: mess += " {:5.1f}".format(dt) print(mess.format(np.sum(loop_times))) ti += 1 mess = "Collect ... {:5.1f}ms /" loop_times = [ 1000 * (t[ti + N * i + 1] - t[ti + N * i]) for i in range(len(stack_lengths)) ] for dt in loop_times: mess += " {:5.1f}".format(dt) print(mess.format(np.sum(loop_times))) ti += 1 mess = "Augment ... {:5.1f}ms /" loop_times = [ 1000 * (t[ti + N * i + 1] - t[ti + N * i]) for i in range(len(stack_lengths)) ] for dt in loop_times: mess += " {:5.1f}".format(dt) print(mess.format(np.sum(loop_times))) ti += N * (len(stack_lengths) - 1) + 1 print("concat .... {:5.1f}ms".format(1000 * (t[ti + 1] - t[ti]))) ti += 1 print("input ..... {:5.1f}ms".format(1000 * (t[ti + 1] - t[ti]))) ti += 1 print("stack ..... {:5.1f}ms".format(1000 * (t[ti + 1] - t[ti]))) ti += 1 print("\n************************\n") return input_list def random_item(self, batch_i): # Initiate concatanation lists p_list = [] f_list = [] l_list = [] i_list = [] pi_list = [] ci_list = [] s_list = [] R_list = [] batch_n = 0 failed_attempts = 0 while True: with self.worker_lock: # Get potential minimum cloud_ind = int(self.epoch_inds[0, self.epoch_i]) point_ind = int(self.epoch_inds[1, self.epoch_i]) # Update epoch indice self.epoch_i += 1 if self.epoch_i >= int(self.epoch_inds.shape[1]): self.epoch_i -= int(self.epoch_inds.shape[1]) # Get points from tree structure points = np.array(self.input_trees[cloud_ind].data, copy=False) # Center point of input region center_point = points[point_ind, :].reshape(1, -1) # Add a small noise to center point if self.set != "ERF": center_point += np.random.normal( scale=self.config.in_radius / 10, size=center_point.shape ) # Indices of points in input region input_inds = self.input_trees[cloud_ind].query_radius( center_point, r=self.config.in_radius )[0] # Number collected n = input_inds.shape[0] # Safe check for empty spheres if n < 2: failed_attempts += 1 if failed_attempts > 100 * self.config.batch_num: raise ValueError( "It seems this dataset only containes empty input spheres" ) continue # Collect labels and colors input_points = (points[input_inds] - center_point).astype(np.float32) input_colors = self.input_colors[cloud_ind][input_inds] if self.set in ["test", "ERF"]: input_labels = np.zeros(input_points.shape[0]) else: input_labels = self.input_labels[cloud_ind][input_inds] input_labels = np.array([self.label_to_idx[l] for l in input_labels]) # Data augmentation input_points, scale, R = self.augmentation_transform(input_points) # Color augmentation if np.random.rand() > self.config.augment_color: input_colors *= 0 # Get original height as additional feature input_features = np.hstack( (input_colors, input_points[:, 2:] + center_point[:, 2:]) ).astype(np.float32) # Stack batch p_list += [input_points] f_list += [input_features] l_list += [input_labels] pi_list += [input_inds] i_list += [point_ind] ci_list += [cloud_ind] s_list += [scale] R_list += [R] # Update batch size batch_n += n # In case batch is full, stop if batch_n > int(self.batch_limit): break # Randomly drop some points (act as an augmentation process and a safety for GPU memory consumption) # if n > int(self.batch_limit): # input_inds = np.random.choice(input_inds, size=int(self.batch_limit) - 1, replace=False) # n = input_inds.shape[0] ################### # Concatenate batch ################### stacked_points = np.concatenate(p_list, axis=0) features = np.concatenate(f_list, axis=0) labels = np.concatenate(l_list, axis=0) point_inds = np.array(i_list, dtype=np.int32) cloud_inds = np.array(ci_list, dtype=np.int32) input_inds = np.concatenate(pi_list, axis=0) stack_lengths = np.array([pp.shape[0] for pp in p_list], dtype=np.int32) scales = np.array(s_list, dtype=np.float32) rots = np.stack(R_list, axis=0) # Input features stacked_features = np.ones_like(stacked_points[:, :1], dtype=np.float32) if self.config.in_features_dim == 1: pass elif self.config.in_features_dim == 4: stacked_features = np.hstack((stacked_features, features[:, :3])) elif self.config.in_features_dim == 5: stacked_features = np.hstack((stacked_features, features)) else: raise ValueError( "Only accepted input dimensions are 1, 4 and 7 (without and with XYZ)" ) ####################### # Create network inputs ####################### # # Points, neighbors, pooling indices for each layers # # Get the whole input list input_list = self.segmentation_inputs( stacked_points, stacked_features, labels, stack_lengths ) # Add scale and rotation for testing input_list += [scales, rots, cloud_inds, point_inds, input_inds] return input_list def prepare_S3DIS_ply(self): print("\nPreparing ply files") t0 = time.time() # Folder for the ply files ply_path = join(self.path, self.train_path) if not exists(ply_path): makedirs(ply_path) for cloud_name in self.cloud_names: # Pass if the cloud has already been computed cloud_file = join(ply_path, cloud_name + ".ply") if exists(cloud_file): continue # Get rooms of the current cloud cloud_folder = join(self.path, cloud_name) room_folders = [ join(cloud_folder, room) for room in listdir(cloud_folder) if isdir(join(cloud_folder, room)) ] # Initiate containers cloud_points = np.empty((0, 3), dtype=np.float32) cloud_colors = np.empty((0, 3), dtype=np.uint8) cloud_classes = np.empty((0, 1), dtype=np.int32) # Loop over rooms for i, room_folder in enumerate(room_folders): print( "Cloud %s - Room %d/%d : %s" % (cloud_name, i + 1, len(room_folders), room_folder.split("/")[-1]) ) for object_name in listdir(join(room_folder, "Annotations")): if object_name[-4:] == ".txt": # Text file containing point of the object object_file = join(room_folder, "Annotations", object_name) # Object class and ID tmp = object_name[:-4].split("_")[0] if tmp in self.name_to_label: object_class = self.name_to_label[tmp] elif tmp in ["stairs"]: object_class = self.name_to_label["clutter"] else: raise ValueError("Unknown object name: " + str(tmp)) # Correct bug in S3DIS dataset if object_name == "ceiling_1.txt": with open(object_file, "r") as f: lines = f.readlines() for l_i, line in enumerate(lines): if "103.0\x100000" in line: lines[l_i] = line.replace( "103.0\x100000", "103.000000" ) with open(object_file, "w") as f: f.writelines(lines) # Read object points and colors object_data = np.loadtxt(object_file, dtype=np.float32) # Stack all data cloud_points = np.vstack( (cloud_points, object_data[:, 0:3].astype(np.float32)) ) cloud_colors = np.vstack( (cloud_colors, object_data[:, 3:6].astype(np.uint8)) ) object_classes = np.full( (object_data.shape[0], 1), object_class, dtype=np.int32 ) cloud_classes = np.vstack((cloud_classes, object_classes)) # Save as ply write_ply( cloud_file, (cloud_points, cloud_colors, cloud_classes), ["x", "y", "z", "red", "green", "blue", "class"], ) print("Done in {:.1f}s".format(time.time() - t0)) return def load_subsampled_clouds(self): # Parameter dl = self.config.first_subsampling_dl # Create path for files tree_path = join(self.path, "input_{:.3f}".format(dl)) if not exists(tree_path): makedirs(tree_path) ############## # Load KDTrees ############## for i, file_path in enumerate(self.files): # Restart timer t0 = time.time() # Get cloud name cloud_name = self.cloud_names[i] # Name of the input files KDTree_file = join(tree_path, "{:s}_KDTree.pkl".format(cloud_name)) sub_ply_file = join(tree_path, "{:s}.ply".format(cloud_name)) # Check if inputs have already been computed if exists(KDTree_file): print( "\nFound KDTree for cloud {:s}, subsampled at {:.3f}".format( cloud_name, dl ) ) # read ply with data data = read_ply(sub_ply_file) sub_colors = np.vstack((data["red"], data["green"], data["blue"])).T sub_labels = data["class"] # Read pkl with search tree with open(KDTree_file, "rb") as f: search_tree = pickle.load(f) else: print( "\nPreparing KDTree for cloud {:s}, subsampled at {:.3f}".format( cloud_name, dl ) ) # Read ply file data = read_ply(file_path) points = np.vstack((data["x"], data["y"], data["z"])).T colors = np.vstack((data["red"], data["green"], data["blue"])).T labels = data["class"] # Subsample cloud sub_points, sub_colors, sub_labels = grid_subsampling( points, features=colors, labels=labels, sampleDl=dl ) # Rescale float color and squeeze label sub_colors = sub_colors / 255 sub_labels = np.squeeze(sub_labels) # Get chosen neighborhoods search_tree = KDTree(sub_points, leaf_size=10) # search_tree = nnfln.KDTree(n_neighbors=1, metric='L2', leaf_size=10) # search_tree.fit(sub_points) # Save KDTree with open(KDTree_file, "wb") as f: pickle.dump(search_tree, f) # Save ply write_ply( sub_ply_file, [sub_points, sub_colors, sub_labels], ["x", "y", "z", "red", "green", "blue", "class"], ) # Fill data containers self.input_trees += [search_tree] self.input_colors += [sub_colors] self.input_labels += [sub_labels] size = sub_colors.shape[0] * 4 * 7 print("{:.1f} MB loaded in {:.1f}s".format(size * 1e-6, time.time() - t0)) ############################ # Coarse potential locations ############################ # Only necessary for validation and test sets if self.use_potentials: print("\nPreparing potentials") # Restart timer t0 = time.time() pot_dl = self.config.in_radius / 10 cloud_ind = 0 for i, file_path in enumerate(self.files): # Get cloud name cloud_name = self.cloud_names[i] # Name of the input files coarse_KDTree_file = join( tree_path, "{:s}_coarse_KDTree.pkl".format(cloud_name) ) # Check if inputs have already been computed if exists(coarse_KDTree_file): # Read pkl with search tree with open(coarse_KDTree_file, "rb") as f: search_tree = pickle.load(f) else: # Subsample cloud sub_points = np.array(self.input_trees[cloud_ind].data, copy=False) coarse_points = grid_subsampling( sub_points.astype(np.float32), sampleDl=pot_dl ) # Get chosen neighborhoods search_tree = KDTree(coarse_points, leaf_size=10) # Save KDTree with open(coarse_KDTree_file, "wb") as f: pickle.dump(search_tree, f) # Fill data containers self.pot_trees += [search_tree] cloud_ind += 1 print("Done in {:.1f}s".format(time.time() - t0)) ###################### # Reprojection indices ###################### # Get number of clouds self.num_clouds = len(self.input_trees) # Only necessary for validation and test sets if self.set in ["validation", "test"]: print("\nPreparing reprojection indices for testing") # Get validation/test reprojection indices for i, file_path in enumerate(self.files): # Restart timer t0 = time.time() # Get info on this cloud cloud_name = self.cloud_names[i] # File name for saving proj_file = join(tree_path, "{:s}_proj.pkl".format(cloud_name)) # Try to load previous indices if exists(proj_file): with open(proj_file, "rb") as f: proj_inds, labels = pickle.load(f) else: data = read_ply(file_path) points = np.vstack((data["x"], data["y"], data["z"])).T labels = data["class"] # Compute projection inds idxs = self.input_trees[i].query(points, return_distance=False) # dists, idxs = self.input_trees[i_cloud].kneighbors(points) proj_inds = np.squeeze(idxs).astype(np.int32) # Save with open(proj_file, "wb") as f: pickle.dump([proj_inds, labels], f) self.test_proj += [proj_inds] self.validation_labels += [labels] print("{:s} done in {:.1f}s".format(cloud_name, time.time() - t0)) print() return def load_evaluation_points(self, file_path): """ Load points (from test or validation split) on which the metrics should be evaluated """ # Get original points data = read_ply(file_path) return np.vstack((data["x"], data["y"], data["z"])).T # ---------------------------------------------------------------------------------------------------------------------- # # Utility classes definition # \********************************/ class S3DISSampler(Sampler): """Sampler for S3DIS""" def __init__(self, dataset: S3DISDataset): Sampler.__init__(self, dataset) # Dataset used by the sampler (no copy is made in memory) self.dataset = dataset # Number of step per epoch if dataset.set == "training": self.N = dataset.config.epoch_steps else: self.N = dataset.config.validation_size return def __iter__(self): """ Yield next batch indices here. In this dataset, this is a dummy sampler that yield the index of batch element (input sphere) in epoch instead of the list of point indices """ if not self.dataset.use_potentials: # Initiate current epoch ind self.dataset.epoch_i *= 0 self.dataset.epoch_inds *= 0 # Initiate container for indices all_epoch_inds = np.zeros((2, 0), dtype=np.int64) # Number of sphere centers taken per class in each cloud num_centers = self.N * self.dataset.config.batch_num random_pick_n = int(np.ceil(num_centers / self.dataset.config.num_classes)) # Choose random points of each class for each cloud np.zeros((2, 0), dtype=np.int64) for label_ind, label in enumerate(self.dataset.label_values): if label not in self.dataset.ignored_labels: # Gather indices of the points with this label in all the input clouds all_label_indices = [] for cloud_ind, cloud_labels in enumerate(self.dataset.input_labels): label_indices = np.where(np.equal(cloud_labels, label))[0] all_label_indices.append( np.vstack( ( np.full( label_indices.shape, cloud_ind, dtype=np.int64 ), label_indices, ) ) ) # Stack them: [2, N1+N2+...] all_label_indices = np.hstack(all_label_indices) # Select a a random number amongst them N_inds = all_label_indices.shape[1] if N_inds < random_pick_n: chosen_label_inds = np.zeros((2, 0), dtype=np.int64) while chosen_label_inds.shape[1] < random_pick_n: chosen_label_inds = np.hstack( ( chosen_label_inds, all_label_indices[:, np.random.permutation(N_inds)], ) ) warnings.warn( "When choosing random epoch indices (use_potentials=False), \ class {:d}: {:s} only had {:d} available points, while we \ needed {:d}. Repeating indices in the same epoch".format( label, self.dataset.label_names[label_ind], N_inds, random_pick_n, ) ) elif N_inds < 50 * random_pick_n: rand_inds = np.random.choice( N_inds, size=random_pick_n, replace=False ) chosen_label_inds = all_label_indices[:, rand_inds] else: chosen_label_inds = np.zeros((2, 0), dtype=np.int64) while chosen_label_inds.shape[1] < random_pick_n: rand_inds = np.unique( np.random.choice( N_inds, size=2 * random_pick_n, replace=True ) ) chosen_label_inds = np.hstack( (chosen_label_inds, all_label_indices[:, rand_inds]) ) chosen_label_inds = chosen_label_inds[:, :random_pick_n] # Stack for each label all_epoch_inds = np.hstack((all_epoch_inds, chosen_label_inds)) # Random permutation of the indices random_order = np.random.permutation(all_epoch_inds.shape[1])[:num_centers] all_epoch_inds = all_epoch_inds[:, random_order].astype(np.int64) # Update epoch inds self.dataset.epoch_inds += torch.from_numpy(all_epoch_inds) # Generator loop for i in range(self.N): yield i def __len__(self): """ The number of yielded samples is variable """ return self.N def fast_calib(self): """ This method calibrates the batch sizes while ensuring the potentials are well initialized. Indeed on a dataset like Semantic3D, before potential have been updated over the dataset, there are cahnces that all the dense area are picked in the begining and in the end, we will have very large batch of small point clouds :return: """ # Estimated average batch size and target value estim_b = 0 target_b = self.dataset.config.batch_num # Calibration parameters low_pass_T = 10 Kp = 100.0 finer = False breaking = False # Convergence parameters smooth_errors = [] converge_threshold = 0.1 t = [time.time()] last_display = time.time() mean_dt = np.zeros(2) for epoch in range(10): for i, test in enumerate(self): # New time t = t[-1:] t += [time.time()] # batch length b = len(test) # Update estim_b (low pass filter) estim_b += (b - estim_b) / low_pass_T # Estimate error (noisy) error = target_b - b # Save smooth errors for convergene check smooth_errors.append(target_b - estim_b) if len(smooth_errors) > 10: smooth_errors = smooth_errors[1:] # Update batch limit with P controller self.dataset.batch_limit += Kp * error # finer low pass filter when closing in if not finer and np.abs(estim_b - target_b) < 1: low_pass_T = 100 finer = True # Convergence if finer and np.max(np.abs(smooth_errors)) < converge_threshold: breaking = True break # Average timing t += [time.time()] mean_dt = 0.9 * mean_dt + 0.1 * (np.array(t[1:]) - np.array(t[:-1])) # Console display (only one per second) if (t[-1] - last_display) > 1.0: last_display = t[-1] message = "Step {:5d} estim_b ={:5.2f} batch_limit ={:7d}, // {:.1f}ms {:.1f}ms" print( message.format( i, estim_b, int(self.dataset.batch_limit), 1000 * mean_dt[0], 1000 * mean_dt[1], ) ) if breaking: break def calibration( self, dataloader, untouched_ratio=0.9, verbose=False, force_redo=False ): """ Method performing batch and neighbors calibration. Batch calibration: Set "batch_limit" (the maximum number of points allowed in every batch) so that the average batch size (number of stacked pointclouds) is the one asked. Neighbors calibration: Set the "neighborhood_limits" (the maximum number of neighbors allowed in convolutions) so that 90% of the neighborhoods remain untouched. There is a limit for each layer. """ ############################## # Previously saved calibration ############################## print("\nStarting Calibration (use verbose=True for more details)") t0 = time.time() redo = force_redo # Batch limit # *********** # Load batch_limit dictionary batch_lim_file = join(self.dataset.path, "batch_limits.pkl") if exists(batch_lim_file): with open(batch_lim_file, "rb") as file: batch_lim_dict = pickle.load(file) else: batch_lim_dict = {} # Check if the batch limit associated with current parameters exists if self.dataset.use_potentials: sampler_method = "potentials" else: sampler_method = "random" key = "{:s}_{:.3f}_{:.3f}_{:d}".format( sampler_method, self.dataset.config.in_radius, self.dataset.config.first_subsampling_dl, self.dataset.config.batch_num, ) if not redo and key in batch_lim_dict: self.dataset.batch_limit[0] = batch_lim_dict[key] else: redo = True if verbose: print("\nPrevious calibration found:") print("Check batch limit dictionary") if key in batch_lim_dict: color = bcolors.OKGREEN v = str(int(batch_lim_dict[key])) else: color = bcolors.FAIL v = "?" print('{:}"{:s}": {:s}{:}'.format(color, key, v, bcolors.ENDC)) # Neighbors limit # *************** # Load neighb_limits dictionary neighb_lim_file = join(self.dataset.path, "neighbors_limits.pkl") if exists(neighb_lim_file): with open(neighb_lim_file, "rb") as file: neighb_lim_dict = pickle.load(file) else: neighb_lim_dict = {} # Check if the limit associated with current parameters exists (for each layer) neighb_limits = [] for layer_ind in range(self.dataset.config.num_layers): dl = self.dataset.config.first_subsampling_dl * (2**layer_ind) if self.dataset.config.deform_layers[layer_ind]: r = dl * self.dataset.config.deform_radius else: r = dl * self.dataset.config.conv_radius key = "{:.3f}_{:.3f}".format(dl, r) if key in neighb_lim_dict: neighb_limits += [neighb_lim_dict[key]] if not redo and len(neighb_limits) == self.dataset.config.num_layers: self.dataset.neighborhood_limits = neighb_limits else: redo = True if verbose: print("Check neighbors limit dictionary") for layer_ind in range(self.dataset.config.num_layers): dl = self.dataset.config.first_subsampling_dl * (2**layer_ind) if self.dataset.config.deform_layers[layer_ind]: r = dl * self.dataset.config.deform_radius else: r = dl * self.dataset.config.conv_radius key = "{:.3f}_{:.3f}".format(dl, r) if key in neighb_lim_dict: color = bcolors.OKGREEN v = str(neighb_lim_dict[key]) else: color = bcolors.FAIL v = "?" print('{:}"{:s}": {:s}{:}'.format(color, key, v, bcolors.ENDC)) if redo: ############################ # Neighbors calib parameters ############################ # From config parameter, compute higher bound of neighbors number in a neighborhood hist_n = int( np.ceil(4 / 3 * np.pi * (self.dataset.config.deform_radius + 1) ** 3) ) # Histogram of neighborhood sizes neighb_hists = np.zeros( (self.dataset.config.num_layers, hist_n), dtype=np.int32 ) ######################## # Batch calib parameters ######################## # Estimated average batch size and target value estim_b = 0 target_b = self.dataset.config.batch_num # Expected batch size order of magnitude expected_N = 100000 # Calibration parameters. Higher means faster but can also become unstable # Reduce Kp and Kd if your GP Uis small as the total number of points per batch will be smaller low_pass_T = 100 Kp = expected_N / 200 Ki = 0.001 * Kp Kd = 5 * Kp finer = False stabilized = False # Convergence parameters smooth_errors = [] converge_threshold = 0.1 # Loop parameters last_display = time.time() i = 0 breaking = False error_I = 0 error_D = 0 last_error = 0 debug_in = [] debug_out = [] debug_b = [] debug_estim_b = [] ##################### # Perform calibration ##################### # number of batch per epoch sample_batches = 999 for epoch in range((sample_batches // self.N) + 1): for batch_i, batch in enumerate(dataloader): # Update neighborhood histogram counts = [ np.sum(neighb_mat.numpy() < neighb_mat.shape[0], axis=1) for neighb_mat in batch.neighbors ] hists = [np.bincount(c, minlength=hist_n)[:hist_n] for c in counts] neighb_hists += np.vstack(hists) # batch length b = len(batch.cloud_inds) # Update estim_b (low pass filter) estim_b += (b - estim_b) / low_pass_T # Estimate error (noisy) error = target_b - b error_I += error error_D = error - last_error last_error = error # Save smooth errors for convergene check smooth_errors.append(target_b - estim_b) if len(smooth_errors) > 30: smooth_errors = smooth_errors[1:] # Update batch limit with P controller self.dataset.batch_limit += Kp * error + Ki * error_I + Kd * error_D # Unstability detection if not stabilized and self.dataset.batch_limit < 0: Kp *= 0.1 Ki *= 0.1 Kd *= 0.1 stabilized = True # finer low pass filter when closing in if not finer and np.abs(estim_b - target_b) < 1: low_pass_T = 100 finer = True # Convergence if finer and np.max(np.abs(smooth_errors)) < converge_threshold: breaking = True break i += 1 t = time.time() # Console display (only one per second) if verbose and (t - last_display) > 1.0: last_display = t message = "Step {:5d} estim_b ={:5.2f} batch_limit ={:7d}" print(message.format(i, estim_b, int(self.dataset.batch_limit))) # Debug plots debug_in.append(int(batch.points[0].shape[0])) debug_out.append(int(self.dataset.batch_limit)) debug_b.append(b) debug_estim_b.append(estim_b) if breaking: break # Plot in case we did not reach convergence if not breaking: import matplotlib.pyplot as plt print( "ERROR: It seems that the calibration have not reached convergence. Here are some plot to understand why:" ) print("If you notice unstability, reduce the expected_N value") print("If convergece is too slow, increase the expected_N value") plt.figure() plt.plot(debug_in) plt.plot(debug_out) plt.figure() plt.plot(debug_b) plt.plot(debug_estim_b) plt.show() # Use collected neighbor histogram to get neighbors limit cumsum = np.cumsum(neighb_hists.T, axis=0) percentiles = np.sum( cumsum < (untouched_ratio * cumsum[hist_n - 1, :]), axis=0 ) self.dataset.neighborhood_limits = percentiles if verbose: # Crop histogram while np.sum(neighb_hists[:, -1]) == 0: neighb_hists = neighb_hists[:, :-1] hist_n = neighb_hists.shape[1] print("\n**************************************************\n") line0 = "neighbors_num " for layer in range(neighb_hists.shape[0]): line0 += "| layer {:2d} ".format(layer) print(line0) for neighb_size in range(hist_n): line0 = " {:4d} ".format(neighb_size) for layer in range(neighb_hists.shape[0]): if neighb_size > percentiles[layer]: color = bcolors.FAIL else: color = bcolors.OKGREEN line0 += "|{:}{:10d}{:} ".format( color, neighb_hists[layer, neighb_size], bcolors.ENDC ) print(line0) print("\n**************************************************\n") print("\nchosen neighbors limits: ", percentiles) print() # Save batch_limit dictionary if self.dataset.use_potentials: sampler_method = "potentials" else: sampler_method = "random" key = "{:s}_{:.3f}_{:.3f}_{:d}".format( sampler_method, self.dataset.config.in_radius, self.dataset.config.first_subsampling_dl, self.dataset.config.batch_num, ) batch_lim_dict[key] = float(self.dataset.batch_limit) with open(batch_lim_file, "wb") as file: pickle.dump(batch_lim_dict, file) # Save neighb_limit dictionary for layer_ind in range(self.dataset.config.num_layers): dl = self.dataset.config.first_subsampling_dl * (2**layer_ind) if self.dataset.config.deform_layers[layer_ind]: r = dl * self.dataset.config.deform_radius else: r = dl * self.dataset.config.conv_radius key = "{:.3f}_{:.3f}".format(dl, r) neighb_lim_dict[key] = self.dataset.neighborhood_limits[layer_ind] with open(neighb_lim_file, "wb") as file: pickle.dump(neighb_lim_dict, file) print("Calibration done in {:.1f}s\n".format(time.time() - t0)) return class S3DISCustomBatch: """Custom batch definition with memory pinning for S3DIS""" def __init__(self, input_list): # Get rid of batch dimension input_list = input_list[0] # Number of layers L = (len(input_list) - 7) // 5 # Extract input tensors from the list of numpy array ind = 0 self.points = [ torch.from_numpy(nparray) for nparray in input_list[ind : ind + L] ] ind += L self.neighbors = [ torch.from_numpy(nparray) for nparray in input_list[ind : ind + L] ] ind += L self.pools = [ torch.from_numpy(nparray) for nparray in input_list[ind : ind + L] ] ind += L self.upsamples = [ torch.from_numpy(nparray) for nparray in input_list[ind : ind + L] ] ind += L self.lengths = [ torch.from_numpy(nparray) for nparray in input_list[ind : ind + L] ] ind += L self.features = torch.from_numpy(input_list[ind]) ind += 1 self.labels = torch.from_numpy(input_list[ind]) ind += 1 self.scales = torch.from_numpy(input_list[ind]) ind += 1 self.rots = torch.from_numpy(input_list[ind]) ind += 1 self.cloud_inds = torch.from_numpy(input_list[ind]) ind += 1 self.center_inds = torch.from_numpy(input_list[ind]) ind += 1 self.input_inds = torch.from_numpy(input_list[ind]) return def pin_memory(self): """ Manual pinning of the memory """ self.points = [in_tensor.pin_memory() for in_tensor in self.points] self.neighbors = [in_tensor.pin_memory() for in_tensor in self.neighbors] self.pools = [in_tensor.pin_memory() for in_tensor in self.pools] self.upsamples = [in_tensor.pin_memory() for in_tensor in self.upsamples] self.lengths = [in_tensor.pin_memory() for in_tensor in self.lengths] self.features = self.features.pin_memory() self.labels = self.labels.pin_memory() self.scales = self.scales.pin_memory() self.rots = self.rots.pin_memory() self.cloud_inds = self.cloud_inds.pin_memory() self.center_inds = self.center_inds.pin_memory() self.input_inds = self.input_inds.pin_memory() return self def to(self, device): self.points = [in_tensor.to(device) for in_tensor in self.points] self.neighbors = [in_tensor.to(device) for in_tensor in self.neighbors] self.pools = [in_tensor.to(device) for in_tensor in self.pools] self.upsamples = [in_tensor.to(device) for in_tensor in self.upsamples] self.lengths = [in_tensor.to(device) for in_tensor in self.lengths] self.features = self.features.to(device) self.labels = self.labels.to(device) self.scales = self.scales.to(device) self.rots = self.rots.to(device) self.cloud_inds = self.cloud_inds.to(device) self.center_inds = self.center_inds.to(device) self.input_inds = self.input_inds.to(device) return self def unstack_points(self, layer=None): """Unstack the points""" return self.unstack_elements("points", layer) def unstack_neighbors(self, layer=None): """Unstack the neighbors indices""" return self.unstack_elements("neighbors", layer) def unstack_pools(self, layer=None): """Unstack the pooling indices""" return self.unstack_elements("pools", layer) def unstack_elements(self, element_name, layer=None, to_numpy=True): """ Return a list of the stacked elements in the batch at a certain layer. If no layer is given, then return all layers """ if element_name == "points": elements = self.points elif element_name == "neighbors": elements = self.neighbors elif element_name == "pools": elements = self.pools[:-1] else: raise ValueError("Unknown element name: {:s}".format(element_name)) all_p_list = [] for layer_i, layer_elems in enumerate(elements): if layer is None or layer == layer_i: i0 = 0 p_list = [] if element_name == "pools": lengths = self.lengths[layer_i + 1] else: lengths = self.lengths[layer_i] for b_i, length in enumerate(lengths): elem = layer_elems[i0 : i0 + length] if element_name == "neighbors": elem[elem >= self.points[layer_i].shape[0]] = -1 elem[elem >= 0] -= i0 elif element_name == "pools": elem[elem >= self.points[layer_i].shape[0]] = -1 elem[elem >= 0] -= torch.sum(self.lengths[layer_i][:b_i]) i0 += length if to_numpy: p_list.append(elem.numpy()) else: p_list.append(elem) if layer == layer_i: return p_list all_p_list.append(p_list) return all_p_list def S3DISCollate(batch_data): return S3DISCustomBatch(batch_data) # ---------------------------------------------------------------------------------------------------------------------- # # Debug functions # \*********************/ def debug_upsampling(dataset, loader): """Shows which labels are sampled according to strategy chosen""" for epoch in range(10): for batch_i, batch in enumerate(loader): pc1 = batch.points[1].numpy() pc2 = batch.points[2].numpy() up1 = batch.upsamples[1].numpy() print(pc1.shape, "=>", pc2.shape) print(up1.shape, np.max(up1)) pc2 = np.vstack((pc2, np.zeros_like(pc2[:1, :]))) # Get neighbors distance p0 = pc1[10, :] neighbs0 = up1[10, :] neighbs0 = pc2[neighbs0, :] - p0 d2 = np.sum(neighbs0**2, axis=1) print(neighbs0.shape) print(neighbs0[:5]) print(d2[:5]) print("******************") print("*******************************************") _, counts = np.unique(dataset.input_labels, return_counts=True) print(counts) def debug_timing(dataset, loader): """Timing of generator function""" t = [time.time()] last_display = time.time() mean_dt = np.zeros(2) estim_b = dataset.config.batch_num estim_N = 0 for epoch in range(10): for batch_i, batch in enumerate(loader): # print(batch_i, tuple(points.shape), tuple(normals.shape), labels, indices, in_sizes) # New time t = t[-1:] t += [time.time()] # Update estim_b (low pass filter) estim_b += (len(batch.cloud_inds) - estim_b) / 100 estim_N += (batch.features.shape[0] - estim_N) / 10 # Pause simulating computations time.sleep(0.05) t += [time.time()] # Average timing mean_dt = 0.9 * mean_dt + 0.1 * (np.array(t[1:]) - np.array(t[:-1])) # Console display (only one per second) if (t[-1] - last_display) > -1.0: last_display = t[-1] message = "Step {:08d} -> (ms/batch) {:8.2f} {:8.2f} / batch = {:.2f} - {:.0f}" print( message.format( batch_i, 1000 * mean_dt[0], 1000 * mean_dt[1], estim_b, estim_N ) ) print("************* Epoch ended *************") _, counts = np.unique(dataset.input_labels, return_counts=True) print(counts) def debug_show_clouds(dataset, loader): for epoch in range(10): L = dataset.config.num_layers for batch_i, batch in enumerate(loader): # Print characteristics of input tensors print("\nPoints tensors") for i in range(L): print(batch.points[i].dtype, batch.points[i].shape) print("\nNeigbors tensors") for i in range(L): print(batch.neighbors[i].dtype, batch.neighbors[i].shape) print("\nPools tensors") for i in range(L): print(batch.pools[i].dtype, batch.pools[i].shape) print("\nStack lengths") for i in range(L): print(batch.lengths[i].dtype, batch.lengths[i].shape) print("\nFeatures") print(batch.features.dtype, batch.features.shape) print("\nLabels") print(batch.labels.dtype, batch.labels.shape) print("\nAugment Scales") print(batch.scales.dtype, batch.scales.shape) print("\nAugment Rotations") print(batch.rots.dtype, batch.rots.shape) print("\nModel indices") print(batch.model_inds.dtype, batch.model_inds.shape) print("\nAre input tensors pinned") print(batch.neighbors[0].is_pinned()) print(batch.neighbors[-1].is_pinned()) print(batch.points[0].is_pinned()) print(batch.points[-1].is_pinned()) print(batch.labels.is_pinned()) print(batch.scales.is_pinned()) print(batch.rots.is_pinned()) print(batch.model_inds.is_pinned()) show_input_batch(batch) print("*******************************************") _, counts = np.unique(dataset.input_labels, return_counts=True) print(counts) def debug_batch_and_neighbors_calib(dataset, loader): """Timing of generator function""" t = [time.time()] last_display = time.time() mean_dt = np.zeros(2) for epoch in range(10): for batch_i, input_list in enumerate(loader): # print(batch_i, tuple(points.shape), tuple(normals.shape), labels, indices, in_sizes) # New time t = t[-1:] t += [time.time()] # Pause simulating computations time.sleep(0.01) t += [time.time()] # Average timing mean_dt = 0.9 * mean_dt + 0.1 * (np.array(t[1:]) - np.array(t[:-1])) # Console display (only one per second) if (t[-1] - last_display) > 1.0: last_display = t[-1] message = "Step {:08d} -> Average timings (ms/batch) {:8.2f} {:8.2f} " print(message.format(batch_i, 1000 * mean_dt[0], 1000 * mean_dt[1])) print("************* Epoch ended *************") _, counts = np.unique(dataset.input_labels, return_counts=True) print(counts)