├── auxiliary ├── __init__.py ├── shaders │ ├── empty.frag │ ├── empty.vert │ ├── passthrough.frag │ ├── check_uniforms.vert │ ├── draw_voxels.frag │ ├── draw_pose.geom │ └── draw_voxels.vert ├── filelist2files.py ├── np_ioueval.py ├── torch_ioueval.py ├── vispy_manager.py ├── SSCDataset.py ├── laserscancomp.py ├── camera.py ├── glow.py ├── laserscanvis.py ├── laserscan.py └── eval_np.py ├── .pep8 ├── .gitignore ├── requirements.txt ├── docker.sh ├── LICENSE ├── count.py ├── content.py ├── Dockerfile ├── remap_semantic_labels.py ├── config ├── semantic-kitti-coarse.yaml ├── semantic-kitti.yaml ├── semantic-kitti-all.yaml └── semantic-kitti-mos.yaml ├── visualize_mos.py ├── compare.py ├── visualize.py ├── generate_sequential.py ├── evaluate_mos.py ├── evaluate_completion.py ├── validate_submission.py ├── evaluate_semantics.py ├── evaluate_semantics_by_distance.py ├── evaluate_panoptic.py └── README.md /auxiliary/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.pep8: -------------------------------------------------------------------------------- 1 | [pep8] 2 | max-line-length = 120 3 | indent-size = 2 -------------------------------------------------------------------------------- /auxiliary/shaders/empty.frag: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | 4 | void main() 5 | { 6 | 7 | } -------------------------------------------------------------------------------- /auxiliary/shaders/empty.vert: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | 4 | void main() 5 | { 6 | 7 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.o 4 | *.so 5 | *.a 6 | **build 7 | .vscode 8 | *.cxx 9 | RangeNet.py 10 | 11 | -------------------------------------------------------------------------------- /auxiliary/shaders/passthrough.frag: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | in vec4 color; 4 | out vec4 out_color; 5 | 6 | void main() 7 | { 8 | out_color = color; 9 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | matplotlib>=2.2.3 2 | vispy>=0.5.3 3 | torch>=1.1.0 4 | numpy>=1.24.0 5 | PyYAML>=5.1.1 6 | imgui[glfw]>=1.0.0 7 | glfw>=1.8.3 8 | PyOpenGL>=3.1.0 9 | pyqt5>=5.8.1.1 10 | -------------------------------------------------------------------------------- /docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This file is covered by the LICENSE file in the root of this project. 3 | docker build -t api --build-arg uid=$(id -g) --build-arg gid=$(id -g) . 4 | docker run --privileged \ 5 | -ti --rm -e DISPLAY=$DISPLAY \ 6 | -v /tmp/.X11-unix:/tmp/.X11-unix \ 7 | -v $1:/home/developer/data/ \ 8 | api -------------------------------------------------------------------------------- /auxiliary/shaders/check_uniforms.vert: -------------------------------------------------------------------------------- 1 | 2 | #version 330 core 3 | 4 | 5 | uniform mat4 test; 6 | 7 | out vec4 color; 8 | 9 | void main() { 10 | 11 | float value = float(gl_VertexID); 12 | 13 | gl_Position = vec4(value/16.0, value/16.0, 0, 1); 14 | if(test[int(value/4)][int(value)%4] == value) 15 | { 16 | color = vec4(0,1,0,1); 17 | } 18 | else 19 | { 20 | color = vec4(1,0,0,1); 21 | } 22 | } -------------------------------------------------------------------------------- /auxiliary/shaders/draw_voxels.frag: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | // simple Blinn-Phong Shading. 4 | 5 | out vec4 out_color; 6 | 7 | in vec4 color; 8 | in vec3 position; 9 | in vec3 normal; 10 | 11 | uniform mat4 view_mat; 12 | uniform vec3 lightPos; 13 | 14 | void main() 15 | { 16 | vec3 viewPos = view_mat[3].xyz; 17 | 18 | vec3 ambient = 0.05 * color.xyz; 19 | 20 | vec3 lightDir = normalize(lightPos - position); 21 | vec3 normal1 = normalize(normal); 22 | float diff = max(dot(lightDir, normal1), 0.0); 23 | vec3 diffuse = diff * color.xyz; 24 | 25 | vec3 viewDir = normalize(viewPos - position); 26 | vec3 reflectDir = reflect(-lightDir, normal); 27 | vec3 halfwayDir = normalize(lightDir + viewDir); 28 | 29 | float spec = pow(max(dot(normal, halfwayDir), 0.0), 32.0); 30 | vec3 specular = vec3(0.1) * spec; 31 | 32 | out_color = vec4(ambient + diffuse + specular, 1.0); 33 | } 34 | -------------------------------------------------------------------------------- /auxiliary/shaders/draw_pose.geom: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | layout(points) in; 4 | layout(line_strip, max_vertices = 6) out; 5 | 6 | uniform mat4 mvp; 7 | uniform mat4 pose; 8 | uniform float size; 9 | 10 | out vec4 color; 11 | 12 | void main() 13 | { 14 | color = vec4(1, 0, 0, 1); 15 | gl_Position = mvp * pose * vec4(0, 0, 0, 1); 16 | EmitVertex(); 17 | gl_Position = mvp * pose * vec4(size, 0, 0, 1); 18 | EmitVertex(); 19 | EndPrimitive(); 20 | 21 | color = vec4(0, 1, 0, 1); 22 | gl_Position = mvp * pose * vec4(0, 0, 0, 1); 23 | EmitVertex(); 24 | gl_Position = mvp * pose * vec4(0, size, 0, 1); 25 | EmitVertex(); 26 | EndPrimitive(); 27 | 28 | color = vec4(0, 0, 1, 1); 29 | gl_Position = mvp * pose * vec4(0, 0, 0, 1); 30 | EmitVertex(); 31 | gl_Position = mvp * pose * vec4(0, 0, size, 1); 32 | EmitVertex(); 33 | EndPrimitive(); 34 | 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2019, University of Bonn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /auxiliary/shaders/draw_voxels.vert: -------------------------------------------------------------------------------- 1 | # version 330 core 2 | 3 | layout(location = 0) in vec3 in_position; 4 | layout(location = 1) in vec3 in_normal; 5 | layout(location = 2) in float in_label; // Note: uint with np.uint32 did not work as expected! 6 | 7 | uniform mat4 mvp; 8 | uniform mat4 view_mat; 9 | uniform sampler2DRect label_colors; 10 | uniform bool use_label_colors; 11 | 12 | uniform ivec3 voxel_dims; 13 | uniform float voxel_size; 14 | uniform float voxel_scale; 15 | uniform vec3 voxel_color; 16 | uniform float voxel_alpha; 17 | 18 | out vec3 position; 19 | out vec3 normal; 20 | out vec4 color; 21 | 22 | 23 | void main() 24 | { 25 | // instance id corresponds to the index in the grid. 26 | vec3 idx; 27 | idx.x = int(float(gl_InstanceID) / float(voxel_dims.y * voxel_dims.z)); 28 | idx.y = int(float(gl_InstanceID - idx.x * voxel_dims.y * voxel_dims.z) / float(voxel_dims.z)); 29 | idx.z = int(gl_InstanceID - idx.x * voxel_dims.y * voxel_dims.z - idx.y * voxel_dims.z); 30 | 31 | // centerize the voxelgrid. 32 | vec3 offset = voxel_size * vec3(0, 0.5, 0.5) * voxel_dims; 33 | vec3 pos = voxel_scale * voxel_size * (in_position - 0.5); // centerize the voxel coordinates and resize. 34 | 35 | position = (view_mat * vec4(pos + idx * voxel_size - offset, 1)).xyz; 36 | normal = (view_mat * vec4(in_normal, 0)).xyz; 37 | 38 | uint label = uint(in_label); 39 | 40 | if(label == uint(0)) // empty voxels 41 | gl_Position = vec4(-10, -10, -10, 1); 42 | else 43 | gl_Position = mvp * vec4(pos + idx * voxel_size - offset, 1); 44 | 45 | color = vec4(voxel_color, voxel_alpha); 46 | if (use_label_colors) color = vec4(texture(label_colors, vec2(label, 0)).rgb, voxel_alpha); 47 | } -------------------------------------------------------------------------------- /count.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # This file is covered by the LICENSE file in the root of this project. 3 | 4 | import argparse 5 | import os 6 | import yaml 7 | import numpy as np 8 | import collections 9 | from auxiliary.laserscan import SemLaserScan 10 | 11 | 12 | if __name__ == '__main__': 13 | parser = argparse.ArgumentParser("./count.py") 14 | parser.add_argument( 15 | '--dataset', '-d', 16 | type=str, 17 | required=True, 18 | help='Dataset to calculate scan count. No Default', 19 | ) 20 | parser.add_argument( 21 | '--config', '-c', 22 | type=str, 23 | required=False, 24 | default="config/semantic-kitti.yaml", 25 | help='Dataset config file. Defaults to %(default)s', 26 | ) 27 | FLAGS, unparsed = parser.parse_known_args() 28 | 29 | # print summary of what we will do 30 | print("*" * 80) 31 | print("INTERFACE:") 32 | print("Dataset", FLAGS.dataset) 33 | print("Config", FLAGS.config) 34 | print("*" * 80) 35 | 36 | # open config file 37 | try: 38 | print("Opening config file %s" % FLAGS.config) 39 | CFG = yaml.safe_load(open(FLAGS.config, 'r')) 40 | except Exception as e: 41 | print(e) 42 | print("Error opening yaml file.") 43 | quit() 44 | 45 | # get training sequences to calculate statistics 46 | sequences = CFG["split"]["train"] 47 | sequences.extend(CFG["split"]["valid"]) 48 | sequences.extend(CFG["split"]["test"]) 49 | sequences.sort() 50 | print("Analizing sequences", sequences, "to count number of scans") 51 | 52 | # iterate over sequences and count scan number 53 | for seq in sequences: 54 | seqstr = '{0:02d}'.format(int(seq)) 55 | scan_paths = os.path.join(FLAGS.dataset, "sequences", 56 | seqstr, "velodyne") 57 | if not os.path.isdir(scan_paths): 58 | print("Sequence", seqstr, "doesn't exist! Exiting...") 59 | quit() 60 | scan_names = [os.path.join(dp, f) for dp, dn, fn in os.walk( 61 | os.path.expanduser(scan_paths)) for f in fn] 62 | print(seqstr, ",", len(scan_names)) 63 | -------------------------------------------------------------------------------- /auxiliary/filelist2files.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import sys 5 | import shutil 6 | import numpy as np 7 | import scipy.io as sio 8 | 9 | from tqdm import tqdm 10 | 11 | def pack(array): 12 | """ convert a boolean array into a bitwise array. """ 13 | array = array.reshape((-1)) 14 | 15 | #compressing bit flags. 16 | # yapf: disable 17 | compressed = array[::8] << 7 | array[1::8] << 6 | array[2::8] << 5 | array[3::8] << 4 | array[4::8] << 3 | array[5::8] << 2 | array[6::8] << 1 | array[7::8] 18 | # yapf: enable 19 | 20 | return np.array(compressed, dtype=np.uint8) 21 | 22 | if __name__ == "__main__": 23 | """ 24 | Convert a given directory of mat files and the given filelist into a separate directory 25 | containing the files in the file list. 26 | """ 27 | 28 | if len(sys.argv) < 2: 29 | print("./filelist2files.py []") 30 | exit(1) 31 | 32 | src_dir = sys.argv[1] 33 | dst_dir = sys.argv[2] 34 | 35 | files = None 36 | 37 | if len(sys.argv) > 3: 38 | files = [line.strip().split("_") for line in open(sys.argv[3])] 39 | else: 40 | 41 | seq_dirs = [d for d in os.listdir(src_dir) if os.path.isdir(os.path.join(src_dir, d))] 42 | files = [] 43 | for d in seq_dirs: 44 | files.extend([(d, os.path.splitext(f)[0]) for f in os.listdir(os.path.join(src_dir, d, "input"))]) 45 | 46 | print("Processing {} files.".format(len(files))) 47 | 48 | for seq_dir, filename in tqdm(files): 49 | 50 | if os.path.exists(os.path.join(src_dir, seq_dir, "input", filename + ".mat")): 51 | data = sio.loadmat(os.path.join(src_dir, seq_dir, "input", filename + ".mat")) 52 | 53 | out_dir = os.path.join(dst_dir, seq_dir, "voxels") 54 | os.makedirs(out_dir, exist_ok=True) 55 | 56 | compressed = pack(data["voxels"]) 57 | compressed.tofile(os.path.join(out_dir, os.path.splitext(filename)[0] + ".bin")) 58 | 59 | if os.path.exists(os.path.join(src_dir, seq_dir, "target_gt", filename + ".mat")): 60 | data = sio.loadmat(os.path.join(src_dir, seq_dir, "target_gt", filename + ".mat")) 61 | 62 | out_dir = os.path.join(dst_dir, seq_dir, "voxels") 63 | os.makedirs(out_dir, exist_ok=True) 64 | 65 | labels = data["voxels"].astype(np.uint16) 66 | labels.tofile(os.path.join(out_dir, os.path.splitext(filename)[0] + ".label")) 67 | 68 | occlusions = pack(data["occluded"]) 69 | occlusions.tofile(os.path.join(out_dir, os.path.splitext(filename)[0] + ".occluded")) 70 | 71 | invalid = pack(data["invalid"]) 72 | invalid.tofile(os.path.join(out_dir, os.path.splitext(filename)[0] + ".invalid")) 73 | -------------------------------------------------------------------------------- /auxiliary/np_ioueval.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # This file is covered by the LICENSE file in the root of this project. 3 | 4 | import sys 5 | import numpy as np 6 | 7 | 8 | class iouEval: 9 | def __init__(self, n_classes, ignore=None): 10 | # classes 11 | self.n_classes = n_classes 12 | 13 | # What to include and ignore from the means 14 | self.ignore = np.array(ignore, dtype=np.int64) 15 | self.include = np.array( 16 | [n for n in range(self.n_classes) if n not in self.ignore], dtype=np.int64) 17 | print("[IOU EVAL] IGNORE: ", self.ignore) 18 | print("[IOU EVAL] INCLUDE: ", self.include) 19 | 20 | # reset the class counters 21 | self.reset() 22 | 23 | def num_classes(self): 24 | return self.n_classes 25 | 26 | def reset(self): 27 | self.conf_matrix = np.zeros((self.n_classes, 28 | self.n_classes), 29 | dtype=np.int64) 30 | 31 | def addBatch(self, x, y): # x=preds, y=targets 32 | # sizes should be matching 33 | x_row = x.reshape(-1) # de-batchify 34 | y_row = y.reshape(-1) # de-batchify 35 | 36 | # check 37 | assert(x_row.shape == y_row.shape) 38 | 39 | # create indexes 40 | idxs = tuple(np.stack((x_row, y_row), axis=0)) 41 | 42 | # make confusion matrix (cols = gt, rows = pred) 43 | np.add.at(self.conf_matrix, idxs, 1) 44 | 45 | def getStats(self): 46 | # remove fp from confusion on the ignore classes cols 47 | conf = self.conf_matrix.copy() 48 | conf[:, self.ignore] = 0 49 | 50 | # get the clean stats 51 | tp = np.diag(conf) 52 | fp = conf.sum(axis=1) - tp 53 | fn = conf.sum(axis=0) - tp 54 | return tp, fp, fn 55 | 56 | def getIoU(self): 57 | tp, fp, fn = self.getStats() 58 | intersection = tp 59 | union = tp + fp + fn + 1e-15 60 | iou = intersection / union 61 | iou_mean = (intersection[self.include] / union[self.include]).mean() 62 | return iou_mean, iou # returns "iou mean", "iou per class" ALL CLASSES 63 | 64 | def getacc(self): 65 | tp, fp, fn = self.getStats() 66 | total_tp = tp.sum() 67 | total = tp[self.include].sum() + fp[self.include].sum() + 1e-15 68 | acc_mean = total_tp / total 69 | return acc_mean # returns "acc mean" 70 | 71 | def get_confusion(self): 72 | return self.conf_matrix.copy() 73 | 74 | 75 | 76 | if __name__ == "__main__": 77 | # mock problem 78 | nclasses = 2 79 | ignore = [] 80 | 81 | # test with 2 squares and a known IOU 82 | lbl = np.zeros((7, 7), dtype=np.int64) 83 | argmax = np.zeros((7, 7), dtype=np.int64) 84 | 85 | # put squares 86 | lbl[2:4, 2:4] = 1 87 | argmax[3:5, 3:5] = 1 88 | 89 | # make evaluator 90 | eval = iouEval(nclasses, ignore) 91 | 92 | # run 93 | eval.addBatch(argmax, lbl) 94 | m_iou, iou = eval.getIoU() 95 | print("IoU: ", m_iou) 96 | print("IoU class: ", iou) 97 | m_acc = eval.getacc() 98 | print("Acc: ", m_acc) 99 | -------------------------------------------------------------------------------- /auxiliary/torch_ioueval.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # This file is covered by the LICENSE file in the root of this project. 3 | 4 | import sys 5 | import torch 6 | import numpy as np 7 | 8 | 9 | class iouEval: 10 | def __init__(self, n_classes, ignore=None): 11 | # classes 12 | self.n_classes = n_classes 13 | 14 | # What to include and ignore from the means 15 | self.ignore = torch.tensor(ignore).long() 16 | self.include = torch.tensor( 17 | [n for n in range(self.n_classes) if n not in self.ignore]).long() 18 | print("[IOU EVAL] IGNORE: ", self.ignore) 19 | print("[IOU EVAL] INCLUDE: ", self.include) 20 | 21 | # get device 22 | self.device = torch.device('cpu') 23 | if torch.cuda.is_available(): 24 | self.device = torch.device('cuda') 25 | 26 | # reset the class counters 27 | self.reset() 28 | 29 | def num_classes(self): 30 | return self.n_classes 31 | 32 | def reset(self): 33 | self.conf_matrix = torch.zeros( 34 | (self.n_classes, self.n_classes), device=self.device).long() 35 | 36 | def addBatch(self, x, y): # x=preds, y=targets 37 | # to tensor 38 | x_row = torch.from_numpy(x).to(self.device).long() 39 | y_row = torch.from_numpy(y).to(self.device).long() 40 | 41 | # sizes should be matching 42 | x_row = x_row.reshape(-1) # de-batchify 43 | y_row = y_row.reshape(-1) # de-batchify 44 | 45 | # check 46 | assert(x_row.shape == x_row.shape) 47 | 48 | # idxs are labels and predictions 49 | idxs = torch.stack([x_row, y_row], dim=0) 50 | 51 | # ones is what I want to add to conf when I 52 | ones = torch.ones((idxs.shape[-1]), device=self.device).long() 53 | 54 | # make confusion matrix (cols = gt, rows = pred) 55 | self.conf_matrix = self.conf_matrix.index_put_( 56 | tuple(idxs), ones, accumulate=True) 57 | 58 | def getStats(self): 59 | # remove fp from confusion on the ignore classes cols 60 | conf = self.conf_matrix.clone().double() 61 | conf[:, self.ignore] = 0 62 | 63 | # get the clean stats 64 | tp = conf.diag() 65 | fp = conf.sum(dim=1) - tp 66 | fn = conf.sum(dim=0) - tp 67 | return tp, fp, fn 68 | 69 | def getIoU(self): 70 | tp, fp, fn = self.getStats() 71 | intersection = tp 72 | union = tp + fp + fn + 1e-15 73 | iou = intersection / union 74 | iou_mean = (intersection[self.include] / union[self.include]).mean() 75 | return iou_mean, iou # returns "iou mean", "iou per class" ALL CLASSES 76 | 77 | def getacc(self): 78 | tp, fp, fn = self.getStats() 79 | total_tp = tp.sum() 80 | total = tp[self.include].sum() + fp[self.include].sum() + 1e-15 81 | acc_mean = total_tp / total 82 | return acc_mean # returns "acc mean" 83 | 84 | 85 | if __name__ == "__main__": 86 | # mock problem 87 | nclasses = 2 88 | ignore = [] 89 | 90 | # test with 2 squares and a known IOU 91 | lbl = np.zeros((7, 7), dtype=np.int64) 92 | argmax = np.zeros((7, 7), dtype=np.int64) 93 | 94 | # put squares 95 | lbl[2:4, 2:4] = 1 96 | argmax[3:5, 3:5] = 1 97 | 98 | # make evaluator 99 | eval = iouEval(nclasses, ignore) 100 | 101 | # run 102 | eval.addBatch(argmax, lbl) 103 | m_iou, iou = eval.getIoU() 104 | print("IoU: ", m_iou) 105 | print("IoU class: ", iou) 106 | m_acc = eval.getacc() 107 | print("Acc: ", m_acc) 108 | -------------------------------------------------------------------------------- /auxiliary/vispy_manager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # This file is covered by the LICENSE file in the root of this project. 3 | 4 | import vispy 5 | from vispy.scene import visuals, SceneCanvas 6 | from abc import ABC, abstractmethod 7 | import numpy as np 8 | 9 | class VispyManager(ABC): 10 | def __init__(self, offset, total, images, instances): 11 | self.canvas, self.grid = self.add_canvas('interactive', 'scan') 12 | self.offset = offset 13 | self.images = images 14 | self.instances = instances 15 | self.n_images = 2 16 | self.img_canvas_W = 1024 17 | self.img_canvas_H = 64 * self.n_images 18 | self.img_canvas = None 19 | if self.images: 20 | self.img_canvas, self.img_grid = self.add_canvas('interactive', 'img', size=(self.img_canvas_W, self.img_canvas_H)) 21 | 22 | self.total = total 23 | 24 | def add_canvas(self, keys, title, size=None): 25 | canvas = None 26 | if size: 27 | canvas = SceneCanvas(keys=keys, show=True, size=size, title=title) 28 | else: 29 | canvas = SceneCanvas(keys=keys, show=True, title=title) 30 | 31 | canvas.events.key_press.connect(self.key_press) 32 | canvas.events.draw.connect(self.draw) 33 | grid = canvas.central_widget.add_grid() 34 | return canvas, grid 35 | 36 | @staticmethod 37 | def block_key_press(canvas): 38 | canvas.events.key_press.block() 39 | 40 | @staticmethod 41 | def key_press_blocked(canvas): 42 | return canvas.events.key_press.blocked() 43 | 44 | @staticmethod 45 | def key_press_unblocked(canvas): 46 | return not canvas.events.key_press.blocked() 47 | 48 | @staticmethod 49 | def unblock_key_press(canvas): 50 | canvas.events.key_press.unblock() 51 | 52 | def add_viewbox(self, row, col, border_color='white'): 53 | view = vispy.scene.widgets.ViewBox(border_color=border_color, parent=self.canvas.scene) 54 | self.grid.add_widget(view, row, col) 55 | vis = visuals.Markers() 56 | view.camera = 'turntable' 57 | view.add(vis) 58 | visuals.XYZAxis(parent=view.scene) 59 | return view, vis 60 | 61 | def add_image_viewbox(self, row, col, border_color='white'): 62 | img_view = vispy.scene.widgets.ViewBox( 63 | border_color=border_color, parent=self.img_canvas.scene) 64 | self.img_grid.add_widget(img_view, row, col) 65 | img_vis = visuals.Image(cmap='viridis') 66 | img_view.add(img_vis) 67 | return img_view, img_vis 68 | 69 | def key_press(self, event): 70 | VispyManager.block_key_press(self.canvas) 71 | if self.img_canvas: 72 | VispyManager.block_key_press(self.img_canvas) 73 | if event.key == 'N': 74 | self.offset += 1 75 | self.offset %= self.total 76 | self.update_scan() 77 | elif event.key == 'B': 78 | self.offset -= 1 79 | self.offset %= self.total 80 | self.update_scan() 81 | elif event.key == 'Q' or event.key == 'Escape': 82 | self.destroy() 83 | 84 | def destroy(self): 85 | if self.canvas: 86 | self.canvas.close() 87 | if self.img_canvas: 88 | self.img_canvas.close() 89 | vispy.app.quit() 90 | 91 | def draw(self, event): 92 | if VispyManager.key_press_blocked(self.canvas): 93 | VispyManager.unblock_key_press(self.canvas) 94 | if self.img_canvas and VispyManager.key_press_blocked(self.img_canvas): 95 | VispyManager.unblock_key_press(self.img_canvas) 96 | 97 | def run(self): 98 | vispy.app.run() 99 | 100 | @abstractmethod 101 | def update_scan(self): 102 | raise NotImplementedError 103 | 104 | @abstractmethod 105 | def reset(self): 106 | raise NotImplementedError 107 | -------------------------------------------------------------------------------- /auxiliary/SSCDataset.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | 4 | 5 | def unpack(compressed): 6 | ''' given a bit encoded voxel grid, make a normal voxel grid out of it. ''' 7 | uncompressed = np.zeros(compressed.shape[0] * 8, dtype=np.uint8) 8 | uncompressed[::8] = compressed[:] >> 7 & 1 9 | uncompressed[1::8] = compressed[:] >> 6 & 1 10 | uncompressed[2::8] = compressed[:] >> 5 & 1 11 | uncompressed[3::8] = compressed[:] >> 4 & 1 12 | uncompressed[4::8] = compressed[:] >> 3 & 1 13 | uncompressed[5::8] = compressed[:] >> 2 & 1 14 | uncompressed[6::8] = compressed[:] >> 1 & 1 15 | uncompressed[7::8] = compressed[:] & 1 16 | 17 | return uncompressed 18 | 19 | 20 | SPLIT_SEQUENCES = { 21 | "train": ["00", "01", "02", "03", "04", "05", "06", "07", "09", "10"], 22 | "valid": ["08"], 23 | "test": ["11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21"] 24 | } 25 | 26 | SPLIT_FILES = { 27 | "train": [".bin", ".label", ".invalid", ".occluded"], 28 | "valid": [".bin", ".label", ".invalid", ".occluded"], 29 | "test": [".bin"] 30 | } 31 | 32 | EXT_TO_NAME = {".bin": "input", ".label": "label", ".invalid": "invalid", ".occluded": "occluded"} 33 | 34 | VOXEL_DIMS = (256, 256, 32) 35 | 36 | 37 | class SSCDataset: 38 | def __init__(self, directory, split="train"): 39 | """ Load data from given dataset directory. """ 40 | 41 | self.files = {} 42 | self.filenames = [] 43 | 44 | for ext in SPLIT_FILES[split]: 45 | self.files[EXT_TO_NAME[ext]] = [] 46 | 47 | for sequence in SPLIT_SEQUENCES[split]: 48 | complete_path = os.path.join(directory, "sequences", sequence, "voxels") 49 | if not os.path.exists(complete_path): raise RuntimeError("Voxel directory missing: " + complete_path) 50 | 51 | files = os.listdir(complete_path) 52 | for ext in SPLIT_FILES[split]: 53 | data = sorted([os.path.join(complete_path, f) for f in files if f.endswith(ext)]) 54 | if len(data) == 0: raise RuntimeError("Missing data for " + EXT_TO_NAME[ext]) 55 | self.files[EXT_TO_NAME[ext]].extend(data) 56 | 57 | # this information is handy for saving the data later, since you need to provide sequences/XX/predictions/000000.label: 58 | self.filenames.extend( 59 | sorted([(sequence, os.path.splitext(f)[0]) for f in files if f.endswith(SPLIT_FILES[split][0])])) 60 | 61 | self.num_files = len(self.filenames) 62 | 63 | # sanity check: 64 | for k, v in self.files.items(): 65 | print(k, len(v)) 66 | assert (len(v) == self.num_files) 67 | 68 | def __len__(self): 69 | return self.num_files 70 | 71 | def __getitem__(self, t): 72 | """ fill dictionary with available data for given index . """ 73 | collection = {} 74 | 75 | # read raw data and unpack (if necessary) 76 | for typ in self.files.keys(): 77 | scan_data = None 78 | if typ == "label": 79 | scan_data = np.fromfile(self.files[typ][t], dtype=np.uint16) 80 | else: 81 | scan_data = unpack(np.fromfile(self.files[typ][t], dtype=np.uint8)) 82 | 83 | # turn in actual voxel grid representation. 84 | collection[typ] = scan_data.reshape(VOXEL_DIMS) 85 | 86 | return self.filenames[t], collection 87 | 88 | 89 | if __name__ == "__main__": 90 | # Small example of the usage. 91 | 92 | # Replace "/path/to/semantic/kitti/" with actual path to the folder containing the "sequences" folder 93 | dataset = SSCDataset("/path/to/semantic/kitti/") 94 | print("# files: {}".format(len(dataset))) 95 | 96 | (seq, filename), data = dataset[100] 97 | print("Contents of entry 100 (" + seq + ":" + filename + "):") 98 | for k, v in data.items(): 99 | print(" {:8} : shape = {}, contents = {}".format(k, v.shape, np.unique(v))) 100 | -------------------------------------------------------------------------------- /auxiliary/laserscancomp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # This file is covered by the LICENSE file in the root of this project. 3 | 4 | from auxiliary.vispy_manager import VispyManager 5 | import numpy as np 6 | 7 | class LaserScanComp(VispyManager): 8 | """Class that creates and handles a side-by-side pointcloud comparison""" 9 | 10 | def __init__(self, scans, scan_names, label_names, offset=0, images=True, instances=False, link=False): 11 | super().__init__(offset, len(scan_names), images, instances) 12 | self.scan_a_view = None 13 | self.scan_a_vis = None 14 | self.scan_b_view = None 15 | self.scan_b_vis = None 16 | self.inst_a_view = None 17 | self.inst_a_vis = None 18 | self.inst_b_view = None 19 | self.inst_b_vis = None 20 | self.img_a_view = None 21 | self.img_a_vis = None 22 | self.img_b_view = None 23 | self.img_b_vis = None 24 | self.img_inst_a_view = None 25 | self.img_inst_a_vis = None 26 | self.img_inst_b_view = None 27 | self.img_inst_b_vis = None 28 | self.scan_a, self.scan_b = scans 29 | self.scan_names = scan_names 30 | self.label_a_names, self.label_b_names = label_names 31 | self.link = link 32 | self.reset() 33 | self.update_scan() 34 | 35 | def reset(self): 36 | """prepares the canvas(es) for the visualizer""" 37 | self.scan_a_view, self.scan_a_vis = super().add_viewbox(0, 0) 38 | self.scan_b_view, self.scan_b_vis = super().add_viewbox(0, 1) 39 | 40 | if self.link: 41 | self.scan_a_view.camera.link(self.scan_b_view.camera) 42 | 43 | if self.images: 44 | self.img_a_view, self.img_a_vis = super().add_image_viewbox(0, 0) 45 | self.img_b_view, self.img_b_vis = super().add_image_viewbox(1, 0) 46 | 47 | if self.instances: 48 | self.img_inst_a_view, self.img_inst_a_vis = super().add_image_viewbox(2, 0) 49 | self.img_inst_b_view, self.img_inst_b_vis = super().add_image_viewbox(3, 0) 50 | 51 | if self.instances: 52 | self.inst_a_view, self.inst_a_vis = super().add_viewbox(1, 0) 53 | self.inst_b_view, self.inst_b_vis = super().add_viewbox(1, 1) 54 | 55 | if self.link: 56 | self.scan_a_view.camera.link(self.inst_a_view.camera) 57 | self.inst_a_view.camera.link(self.inst_b_view.camera) 58 | 59 | def update_scan(self): 60 | """updates the scans, images and instances""" 61 | self.scan_a.open_scan(self.scan_names[self.offset]) 62 | self.scan_a.open_label(self.label_a_names[self.offset]) 63 | self.scan_a.colorize() 64 | self.scan_a_vis.set_data(self.scan_a.points, 65 | face_color=self.scan_a.sem_label_color[..., ::-1], 66 | edge_color=self.scan_a.sem_label_color[..., ::-1], 67 | size=1) 68 | 69 | self.scan_b.open_scan(self.scan_names[self.offset]) 70 | self.scan_b.open_label(self.label_b_names[self.offset]) 71 | self.scan_b.colorize() 72 | self.scan_b_vis.set_data(self.scan_b.points, 73 | face_color=self.scan_b.sem_label_color[..., ::-1], 74 | edge_color=self.scan_b.sem_label_color[..., ::-1], 75 | size=1) 76 | 77 | if self.instances: 78 | self.inst_a_vis.set_data(self.scan_a.points, 79 | face_color=self.scan_a.inst_label_color[..., ::-1], 80 | edge_color=self.scan_a.inst_label_color[..., ::-1], 81 | size=1) 82 | self.inst_b_vis.set_data(self.scan_b.points, 83 | face_color=self.scan_b.inst_label_color[..., ::-1], 84 | edge_color=self.scan_b.inst_label_color[..., ::-1], 85 | size=1) 86 | 87 | if self.images: 88 | self.img_a_vis.set_data(self.scan_a.proj_sem_color[..., ::-1]) 89 | self.img_a_vis.update() 90 | self.img_b_vis.set_data(self.scan_b.proj_sem_color[..., ::-1]) 91 | self.img_b_vis.update() 92 | 93 | if self.instances: 94 | self.img_inst_a_vis.set_data(self.scan_a.proj_inst_color[..., ::-1]) 95 | self.img_inst_a_vis.update() 96 | self.img_inst_b_vis.set_data(self.scan_b.proj_inst_color[..., ::-1]) 97 | self.img_inst_b_vis.update() 98 | 99 | -------------------------------------------------------------------------------- /content.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # This file is covered by the LICENSE file in the root of this project. 3 | 4 | import argparse 5 | import os 6 | import yaml 7 | import numpy as np 8 | import collections 9 | from auxiliary.laserscan import SemLaserScan 10 | 11 | 12 | if __name__ == '__main__': 13 | parser = argparse.ArgumentParser("./content.py") 14 | parser.add_argument( 15 | '--dataset', '-d', 16 | type=str, 17 | required=True, 18 | help='Dataset to calculate content. No Default', 19 | ) 20 | parser.add_argument( 21 | '--config', '-c', 22 | type=str, 23 | required=False, 24 | default="config/semantic-kitti.yaml", 25 | help='Dataset config file. Defaults to %(default)s', 26 | ) 27 | FLAGS, unparsed = parser.parse_known_args() 28 | 29 | # print summary of what we will do 30 | print("*" * 80) 31 | print("INTERFACE:") 32 | print("Dataset", FLAGS.dataset) 33 | print("Config", FLAGS.config) 34 | print("*" * 80) 35 | 36 | # open config file 37 | try: 38 | print("Opening config file %s" % FLAGS.config) 39 | CFG = yaml.safe_load(open(FLAGS.config, 'r')) 40 | except Exception as e: 41 | print(e) 42 | print("Error opening yaml file.") 43 | quit() 44 | 45 | # get training sequences to calculate statistics 46 | sequences = CFG["split"]["train"] 47 | print("Analyzing sequences", sequences) 48 | 49 | # create content accumulator 50 | accum = {} 51 | total = 0.0 52 | for key, _ in CFG["labels"].items(): 53 | accum[key] = 0 54 | 55 | # iterate over sequences 56 | for seq in sequences: 57 | seq_accum = {} 58 | seq_total = 0.0 59 | for key, _ in CFG["labels"].items(): 60 | seq_accum[key] = 0 61 | 62 | # make seq string 63 | print("*" * 80) 64 | seqstr = '{0:02d}'.format(int(seq)) 65 | print("parsing seq {}".format(seq)) 66 | 67 | # does sequence folder exist? 68 | scan_paths = os.path.join(FLAGS.dataset, "sequences", 69 | seqstr, "velodyne") 70 | if os.path.isdir(scan_paths): 71 | print("Sequence folder exists!") 72 | else: 73 | print("Sequence folder doesn't exist! Exiting...") 74 | quit() 75 | 76 | # populate the pointclouds 77 | scan_names = [os.path.join(dp, f) for dp, dn, fn in os.walk( 78 | os.path.expanduser(scan_paths)) for f in fn] 79 | scan_names.sort() 80 | 81 | # does sequence folder exist? 82 | label_paths = os.path.join(FLAGS.dataset, "sequences", 83 | seqstr, "labels") 84 | if os.path.isdir(label_paths): 85 | print("Labels folder exists!") 86 | else: 87 | print("Labels folder doesn't exist! Exiting...") 88 | quit() 89 | # populate the pointclouds 90 | label_names = [os.path.join(dp, f) for dp, dn, fn in os.walk( 91 | os.path.expanduser(label_paths)) for f in fn] 92 | label_names.sort() 93 | 94 | # check that there are same amount of labels and scans 95 | print(len(label_names)) 96 | print(len(scan_names)) 97 | assert(len(label_names) == len(scan_names)) 98 | 99 | # create a scan 100 | scan = SemLaserScan(CFG["color_map"], project=False) 101 | 102 | for idx in range(len(scan_names)): 103 | # open scan 104 | print(label_names[idx]) 105 | scan.open_scan(scan_names[idx]) 106 | scan.open_label(label_names[idx]) 107 | # make histogram and accumulate 108 | count = np.bincount(scan.sem_label) 109 | seq_total += count.sum() 110 | for key, data in seq_accum.items(): 111 | if count.size > key: 112 | seq_accum[key] += count[key] 113 | # zero the count 114 | count[key] = 0 115 | for i, c in enumerate(count): 116 | if c > 0: 117 | print("wrong label ", i, ", nr: ", c) 118 | 119 | seq_accum = collections.OrderedDict( 120 | sorted(seq_accum.items(), key=lambda t: t[0])) 121 | 122 | # print and send to total 123 | total += seq_total 124 | print("seq ", seqstr, "total", seq_total) 125 | for key, data in seq_accum.items(): 126 | accum[key] += data 127 | print(data) 128 | 129 | # print content to fill yaml file 130 | print("*" * 80) 131 | print("Content in training set") 132 | print(accum) 133 | accum = collections.OrderedDict(sorted(accum.items(), key=lambda t: t[0])) 134 | for key, data in accum.items(): 135 | print(" {}: {}".format(key, data / total)) 136 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # This file is covered by the LICENSE file in the root of this project. 2 | 3 | # Use an official ubuntu runtime as a parent image 4 | FROM ubuntu:16.04 5 | 6 | # Install all system pre-reqs 7 | # common pre-reqs 8 | RUN apt update 9 | RUN apt upgrade -y 10 | RUN apt install apt-utils \ 11 | build-essential \ 12 | curl \ 13 | git \ 14 | cmake \ 15 | unzip \ 16 | autoconf \ 17 | autogen \ 18 | libtool \ 19 | mlocate \ 20 | zlib1g-dev \ 21 | python \ 22 | python3-dev \ 23 | python3-pip \ 24 | python3-wheel \ 25 | python3-tk \ 26 | wget \ 27 | libpng-dev \ 28 | libfreetype6-dev \ 29 | vim \ 30 | meld \ 31 | sudo \ 32 | libav-tools \ 33 | python3-pyqt5.qtopengl \ 34 | x11-apps \ 35 | -y 36 | RUN updatedb 37 | 38 | # # Install any python pre-reqs from requirements.txt 39 | RUN curl -fsSL https://bootstrap.pypa.io/pip/3.5/get-pip.py | python3.5 40 | RUN pip3 install scipy==0.19.1 \ 41 | numpy==1.14.0 42 | # torch==0.4.1 \ 43 | 44 | RUN pip3 install torch===0.4.1 -f https://download.pytorch.org/whl/torch_stable.html 45 | RUN pip3 install opencv_python==3.4.0.12 \ 46 | vispy==0.5.3 \ 47 | tensorflow==1.11.0 \ 48 | PyYAML==3.13 \ 49 | enum34==1.1.6 \ 50 | matplotlib==3.0.3 51 | 52 | ENV PYTHONPATH /home/developer/api 53 | 54 | # graphical interface stuff 55 | 56 | # uid and gid 57 | ARG uid=1000 58 | ARG gid=1000 59 | 60 | # echo to make sure that they are the ones from my setup 61 | RUN echo "$uid:$gid" 62 | 63 | # Graphical interface stuff 64 | RUN mkdir -p /home/developer && \ 65 | cp /etc/skel/.bashrc /home/developer/.bashrc && \ 66 | echo "developer:x:${uid}:${gid}:Developer,,,:/home/developer:/bin/bash" >> /etc/passwd && \ 67 | echo "developer:x:${uid}:" >> /etc/group && \ 68 | echo "developer ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/developer && \ 69 | chmod 0440 /etc/sudoers.d/developer && \ 70 | chown ${uid}:${gid} -R /home/developer 71 | 72 | # opengl things 73 | ENV DEBIAN_FRONTEND "noninteractive" 74 | # Install all needed deps 75 | RUN apt install -y xvfb pkg-config \ 76 | llvm-3.9-dev \ 77 | xorg-server-source \ 78 | python-dev \ 79 | x11proto-gl-dev \ 80 | libxext-dev \ 81 | libx11-xcb-dev \ 82 | libxcb-dri2-0-dev \ 83 | libxcb-xfixes0-dev \ 84 | libdrm-dev \ 85 | libx11-dev; 86 | 87 | # compile the mesa llvmpipe driver from source. 88 | RUN mkdir -p /var/tmp/build; \ 89 | cd /var/tmp/build; \ 90 | wget "https://mesa.freedesktop.org/archive/mesa-18.0.1.tar.gz"; \ 91 | tar xfv mesa-18.0.1.tar.gz; \ 92 | rm mesa-18.0.1.tar.gz; \ 93 | cd mesa-18.0.1; \ 94 | ./configure --enable-glx=gallium-xlib --with-gallium-drivers=swrast,swr --disable-dri --disable-gbm --disable-egl --enable-gallium-osmesa --enable-llvm --prefix=/usr/local/ --with-llvm-prefix=/usr/lib/llvm-3.9/; \ 95 | make -j3; \ 96 | make install; \ 97 | cd .. ; \ 98 | rm -rf mesa-18.0.1; 99 | 100 | # install mesa stuff for testing 101 | RUN sudo apt install -y glew-utils libglew-dev freeglut3-dev \ 102 | && wget "ftp://ftp.freedesktop.org/pub/mesa/demos/mesa-demos-8.4.0.tar.gz" \ 103 | && tar xfv mesa-demos-8.4.0.tar.gz \ 104 | && rm mesa-demos-8.4.0.tar.gz \ 105 | && cd mesa-demos-8.4.0 \ 106 | && ./configure --prefix=/usr/local \ 107 | && make -j3 \ 108 | && make install \ 109 | && cd .. \ 110 | && rm -rf mesa-demos-8.4.0 111 | 112 | # clean the cache 113 | RUN apt update && \ 114 | apt autoremove --purge -y && \ 115 | apt clean -y 116 | 117 | ENV XVFB_WHD="1920x1080x24"\ 118 | DISPLAY=":99" \ 119 | LIBGL_ALWAYS_SOFTWARE="1" \ 120 | GALLIUM_DRIVER="swr" \ 121 | LP_NO_RAST="false" \ 122 | LP_DEBUG="" \ 123 | LP_PERF="" \ 124 | LP_NUM_THREADS="" 125 | 126 | # Set the working directory to the api location 127 | WORKDIR /home/developer/api 128 | 129 | # make user and home 130 | USER developer 131 | ENV HOME /home/developer 132 | 133 | # Copy the current directory contents into the container at ~/api 134 | ADD . /home/developer/api -------------------------------------------------------------------------------- /remap_semantic_labels.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # This file is covered by the LICENSE file in the root of this project. 3 | 4 | import argparse 5 | import os 6 | import yaml 7 | import numpy as np 8 | 9 | # possible splits 10 | splits = ["train", "valid", "test"] 11 | 12 | if __name__ == '__main__': 13 | parser = argparse.ArgumentParser("./remap_semantic_labels.py") 14 | parser.add_argument( 15 | '--dataset', '-d', 16 | type=str, 17 | required=False, 18 | default=None, 19 | help='Dataset dir. WARNING: This file remaps the labels in place, so the original labels will be lost. Cannot be used together with -predictions- flag.' 20 | ) 21 | parser.add_argument( 22 | '--predictions', '-p', 23 | type=str, 24 | required=False, 25 | default=None, 26 | help='Prediction dir. WARNING: This file remaps the predictions in place, so the original predictions will be lost. Cannot be used together with -dataset- flag.' 27 | ) 28 | parser.add_argument( 29 | '--split', '-s', 30 | type=str, 31 | required=False, 32 | default="valid", 33 | help='Split to evaluate on. One of ' + 34 | str(splits) + '. Defaults to %(default)s', 35 | ) 36 | parser.add_argument( 37 | '--datacfg', '-dc', 38 | type=str, 39 | required=False, 40 | default="config/semantic-kitti.yaml", 41 | help='Dataset config file. Defaults to %(default)s', 42 | ) 43 | parser.add_argument( 44 | '--inverse', 45 | dest='inverse', 46 | default=False, 47 | action='store_true', 48 | help='Map from xentropy to original, instead of original to xentropy. ' 49 | 'Defaults to %(default)s', 50 | ) 51 | FLAGS, unparsed = parser.parse_known_args() 52 | 53 | # print summary of what we will do 54 | print("*" * 80) 55 | print("INTERFACE:") 56 | print("Data: ", FLAGS.dataset) 57 | print("Predictions: ", FLAGS.predictions) 58 | print("Split: ", FLAGS.split) 59 | print("Config: ", FLAGS.datacfg) 60 | print("Inverse: ", FLAGS.inverse) 61 | print("*" * 80) 62 | 63 | # only predictions or dataset can be handled at once and one MUST be given (xor) 64 | assert((FLAGS.dataset is not None) != (FLAGS.predictions is not None)) 65 | 66 | # check name 67 | root_directory = "" 68 | label_directory = "" 69 | if(FLAGS.dataset is not None): 70 | root_directory = FLAGS.dataset 71 | label_directory = "labels" 72 | elif(FLAGS.predictions is not None): 73 | root_directory = FLAGS.predictions 74 | label_directory = "predictions" 75 | else: 76 | print("I don't even know how I got here") 77 | quit() 78 | 79 | # assert split 80 | assert(FLAGS.split in splits) 81 | 82 | print("Opening data config file %s" % FLAGS.datacfg) 83 | DATA = yaml.safe_load(open(FLAGS.datacfg, 'r')) 84 | 85 | # get number of interest classes, and the label mappings 86 | if FLAGS.inverse: 87 | print("Mapping xentropy to original labels") 88 | remapdict = DATA["learning_map_inv"] 89 | else: 90 | remapdict = DATA["learning_map"] 91 | nr_classes = len(remapdict) 92 | 93 | # make lookup table for mapping 94 | maxkey = max(remapdict.keys()) 95 | 96 | # +100 hack making lut bigger just in case there are unknown labels 97 | remap_lut = np.zeros((maxkey + 100), dtype=np.int32) 98 | remap_lut[list(remapdict.keys())] = list(remapdict.values()) 99 | # print(remap_lut) 100 | 101 | # get wanted set 102 | sequences = [] 103 | sequences.extend(DATA["split"][FLAGS.split]) 104 | 105 | # get label paths 106 | label_names = [] 107 | for sequence in sequences: 108 | sequence = '{0:02d}'.format(int(sequence)) 109 | label_paths = os.path.join(root_directory, "sequences", 110 | sequence, label_directory) 111 | # populate the label names 112 | seq_label_names = [os.path.join(dp, f) for dp, dn, fn in os.walk( 113 | os.path.expanduser(label_paths)) for f in fn if ".label" in f] 114 | seq_label_names.sort() 115 | label_names.extend(seq_label_names) 116 | # print(label_names) 117 | 118 | # open each file, get the tensor, and remap only the lower half (semantics) 119 | for label_file in label_names: 120 | # open label 121 | print(label_file) 122 | label = np.fromfile(label_file, dtype=np.uint32) 123 | label = label.reshape((-1)) 124 | upper_half = label >> 16 # get upper half for instances 125 | lower_half = label & 0xFFFF # get lower half for semantics 126 | lower_half = remap_lut[lower_half] # do the remapping of semantics 127 | label = (upper_half << 16) + lower_half # reconstruct full label 128 | label = label.astype(np.uint32) 129 | label.tofile(label_file) 130 | -------------------------------------------------------------------------------- /config/semantic-kitti-coarse.yaml: -------------------------------------------------------------------------------- 1 | # This file is covered by the LICENSE file in the root of this project. 2 | labels: 3 | 0 : "unlabeled" 4 | 1 : "outlier" 5 | 10: "car" 6 | 11: "bicycle" 7 | 13: "bus" 8 | 15: "motorcycle" 9 | 16: "on-rails" 10 | 18: "truck" 11 | 20: "other-vehicle" 12 | 30: "person" 13 | 31: "bicyclist" 14 | 32: "motorcyclist" 15 | 40: "road" 16 | 44: "parking" 17 | 48: "sidewalk" 18 | 49: "other-ground" 19 | 50: "building" 20 | 51: "fence" 21 | 52: "other-structure" 22 | 60: "lane-marking" 23 | 70: "vegetation" 24 | 71: "trunk" 25 | 72: "terrain" 26 | 80: "pole" 27 | 81: "traffic-sign" 28 | 99: "other-object" 29 | 252: "moving-car" 30 | 253: "moving-bicyclist" 31 | 254: "moving-person" 32 | 255: "moving-motorcyclist" 33 | 256: "moving-on-rails" 34 | 257: "moving-bus" 35 | 258: "moving-truck" 36 | 259: "moving-other-vehicle" 37 | color_map: # bgr 38 | 0 : [0, 0, 0] 39 | 1 : [0, 0, 255] 40 | 10: [245, 150, 100] 41 | 11: [245, 230, 100] 42 | 13: [250, 80, 100] 43 | 15: [150, 60, 30] 44 | 16: [255, 0, 0] 45 | 18: [180, 30, 80] 46 | 20: [255, 0, 0] 47 | 30: [30, 30, 255] 48 | 31: [200, 40, 255] 49 | 32: [90, 30, 150] 50 | 40: [255, 0, 255] 51 | 44: [255, 150, 255] 52 | 48: [75, 0, 75] 53 | 49: [75, 0, 175] 54 | 50: [0, 200, 255] 55 | 51: [50, 120, 255] 56 | 52: [0, 150, 255] 57 | 60: [170, 255, 150] 58 | 70: [0, 175, 0] 59 | 71: [0, 60, 135] 60 | 72: [80, 240, 150] 61 | 80: [150, 240, 255] 62 | 81: [0, 0, 255] 63 | 99: [255, 255, 50] 64 | 252: [245, 150, 100] 65 | 256: [255, 0, 0] 66 | 253: [200, 40, 255] 67 | 254: [30, 30, 255] 68 | 255: [90, 30, 150] 69 | 257: [250, 80, 100] 70 | 258: [180, 30, 80] 71 | 259: [255, 0, 0] 72 | content: # as a ratio with the total number of points 73 | 0: 0.018889854628292943 74 | 1: 0.0002937197336781505 75 | 10: 0.040818519255974316 76 | 11: 0.00016609538710764618 77 | 13: 2.7879693665067774e-05 78 | 15: 0.00039838616015114444 79 | 16: 0.0 80 | 18: 0.0020633612104619787 81 | 20: 0.0016218197275284021 82 | 30: 0.00017698551338515307 83 | 31: 1.1065903904919655e-08 84 | 32: 5.532951952459828e-09 85 | 40: 0.1987493871255525 86 | 44: 0.014717169549888214 87 | 48: 0.14392298360372 88 | 49: 0.0039048553037472045 89 | 50: 0.1326861944777486 90 | 51: 0.0723592229456223 91 | 52: 0.002395131480328884 92 | 60: 4.7084144280367186e-05 93 | 70: 0.26681502148037506 94 | 71: 0.006035012012626033 95 | 72: 0.07814222006271769 96 | 80: 0.002855498193863172 97 | 81: 0.0006155958086189918 98 | 99: 0.009923127583046915 99 | 252: 0.001789309418528068 100 | 253: 0.00012709999297008662 101 | 254: 0.00016059776092534436 102 | 255: 3.745553104802113e-05 103 | 256: 0.0 104 | 257: 0.00011351574470342043 105 | 258: 0.00010157861367183268 106 | 259: 4.3840131989471124e-05 107 | # classes that are indistinguishable from single scan or inconsistent in 108 | # ground truth are mapped to their closest equivalent 109 | learning_map: # according to COLA 110 | 0: 0 111 | 1: 0 112 | 10: 3 113 | 11: 3 114 | 13: 3 115 | 15: 3 116 | 16: 3 117 | 18: 3 118 | 20: 3 119 | 30: 5 120 | 31: 5 121 | 32: 5 122 | 40: 1 123 | 44: 1 124 | 48: 8 125 | 49: 8 126 | 50: 2 127 | 51: 2 128 | 52: 2 129 | 60: 1 130 | 70: 4 131 | 71: 4 132 | 72: 4 133 | 80: 7 134 | 81: 7 135 | 99: 0 136 | 252: 3 137 | 253: 3 138 | 254: 5 139 | 255: 5 140 | 256: 3 141 | 257: 5 142 | 258: 3 143 | 259: 3 144 | learning_map_inv: # inverse of previous map 145 | 0: 0 # "unlabeled", and others ignored 146 | 1: 40 # "drivable ground: parking, road (40)" 147 | 2: 50 # "structure: fence, building (50)" 148 | 3: 10 # "vehicle: motorcycle, car (10), bicycle, truck, other vehicles" 149 | 4: 70 # "nature: trunk, vegetation (70), terrain" 150 | 5: 30 # "living-being: person (30), bicyclist, motorcyclist" 151 | 6: 0 # "dynamic object" 152 | 7: 80 # "static-object: pole (80), sign" 153 | 8: 48 # other ground: sidewalk (48), other ground 154 | learning_ignore: # Ignore classes 155 | 0: True # "unlabeled", and others ignored 156 | 1: False # "drivable ground: parking, road (40)" 157 | 2: False # "structure: fence, building (50)" 158 | 3: False # "vehicle: motorcycle, car (10), bicycle, truck, other vehicles" 159 | 4: False # "nature: trunk, vegetation (70), terrain" 160 | 5: False # "living-being: person (30), bicyclist, motorcyclist" 161 | 6: True # "dynamic object" 162 | 7: False # "static-object: pole (80), sign" 163 | 8: False # other ground: sidewalk (48), other ground 164 | split: # sequence numbers 165 | train: 166 | - 0 167 | - 1 168 | - 2 169 | - 3 170 | - 4 171 | - 5 172 | - 6 173 | - 7 174 | - 9 175 | - 10 176 | valid: 177 | - 8 178 | test: 179 | - 11 180 | - 12 181 | - 13 182 | - 14 183 | - 15 184 | - 16 185 | - 17 186 | - 18 187 | - 19 188 | - 20 189 | - 21 190 | -------------------------------------------------------------------------------- /auxiliary/camera.py: -------------------------------------------------------------------------------- 1 | import math 2 | import numpy as np 3 | import time 4 | import glfw 5 | 6 | def RotX(angle): 7 | sin_t = math.sin(angle) 8 | cos_t = math.cos(angle) 9 | 10 | return np.array([1, 0, 0, 0, 0, cos_t, -sin_t, 0, 0, sin_t, cos_t, 0, 0, 0, 0, 1], dtype=np.float32).reshape(4, 4) 11 | 12 | 13 | def RotY(angle): 14 | sin_t = math.sin(angle) 15 | cos_t = math.cos(angle) 16 | 17 | return np.array([cos_t, 0, sin_t, 0, 0, 1, 0, 0, -sin_t, 0, cos_t, 0, 0, 0, 0, 1], dtype=np.float32).reshape(4, 4) 18 | 19 | 20 | def Trans(x, y, z): 21 | return np.array([1, 0, 0, x, 0, 1, 0, y, 0, 0, 1, z, 0, 0, 0, 1], dtype=np.float32).reshape(4, 4) 22 | 23 | 24 | class Camera: 25 | """ Camera for handling the view matrix based on mouse inputs. """ 26 | 27 | def __init__(self): 28 | self.x_ = self.y_ = self.z_ = 0.0 29 | self.pitch_ = 0.0 30 | self.yaw_ = 0.0 31 | 32 | self.startdrag_ = False 33 | self.startTime_ = 0 34 | self.startx_ = 0 35 | self.starty_ = 0 36 | self.startyaw_ = 0 37 | self.startpitch_ = 0 38 | 39 | self.forwardVel_ = 0.0 40 | self.upVel_ = 0.0 41 | self.sideVel_ = 0.0 42 | self.turnVel_ = 0.0 43 | self.startdrag_ = False 44 | 45 | def lookAt(self, x_cam, y_cam, z_cam, x_ref, y_ref, z_ref): 46 | self.x_ = x_cam 47 | self.y_ = y_cam 48 | self.z_ = z_cam 49 | 50 | x = x_ref - self.x_ 51 | y = y_ref - self.y_ 52 | z = z_ref - self.z_ 53 | length = math.sqrt(x * x + y * y + z * z) 54 | 55 | self.pitch_ = math.asin(y / length) # = std: : acos(-dir.y()) - M_PI_2 in [-pi/2, pi/2] 56 | self.yaw_ = math.atan2(-x, -z) 57 | 58 | self.startdrag_ = False 59 | 60 | @property 61 | def matrix(self): 62 | # current time. 63 | end = time.time() 64 | dt = end - self.startTime_ 65 | 66 | if dt > 0 and self.startdrag_: 67 | # apply velocity & reset timer... 68 | self.rotate(self.turnVel_ * dt, 0.0) 69 | self.translate(self.forwardVel_ * dt, self.upVel_ * dt, self.sideVel_ * dt) 70 | self.startTime_ = end 71 | 72 | # recompute the view matrix (Euler angles) Remember: Inv(AB) = Inv(B)*Inv(A) 73 | # Inv(translate*rotateYaw*rotatePitch) = Inv(rotatePitch)*Inv(rotateYaw)*Inv(translate) 74 | view_ = RotX(-self.pitch_) 75 | view_ = view_ @ RotY(-self.yaw_) 76 | view_ = view_ @ Trans(-self.x_, -self.y_, -self.z_) 77 | 78 | return view_ 79 | 80 | def mousePressed(self, x, y, btn, modifier): 81 | self.startx_ = x 82 | self.starty_ = y 83 | self.startyaw_ = self.yaw_ 84 | self.startpitch_ = self.pitch_ 85 | self.startTime_ = time.time() 86 | self.startdrag_ = True 87 | 88 | return True 89 | 90 | def mouseReleased(self, x, y, btn, modifier): 91 | self.forwardVel_ = 0.0 92 | self.upVel_ = 0.0 93 | self.sideVel_ = 0.0 94 | self.turnVel_ = 0.0 95 | self.startdrag_ = False 96 | 97 | return True 98 | 99 | def translate(self, forward, up, sideways): 100 | # forward = -z, sideways = x , up = y. Remember: inverse of yaw is applied, i.e., we have to apply yaw (?) 101 | # Also keep in mind: sin(-alpha) = -sin(alpha) and cos(-alpha) = -cos(alpha) 102 | # We only apply the yaw to move along the yaw direction; 103 | # x' = x*cos(yaw) - z*sin(yaw) 104 | # z' = x*sin(yaw) + z*cos(yaw) 105 | s = math.sin(self.yaw_) 106 | c = math.cos(self.yaw_) 107 | 108 | self.x_ = self.x_ + sideways * c - forward * s 109 | self.y_ = self.y_ + up 110 | self.z_ = self.z_ - (sideways * s + forward * c) 111 | 112 | def rotate(self, yaw, pitch): 113 | self.yaw_ += yaw 114 | self.pitch_ += pitch 115 | if self.pitch_ < -0.5 * math.pi: 116 | self.pitch_ = -0.5 * math.pi 117 | if self.pitch_ > 0.5 * math.pi: 118 | self.pitch_ = 0.5 * math.pi 119 | 120 | def mouseMoved(self, x, y, btn, modifier): 121 | # some constants. 122 | MIN_MOVE = 0 123 | WALK_SENSITIVITY = 0.5 124 | TURN_SENSITIVITY = 0.01 125 | SLIDE_SENSITIVITY = 0.5 126 | RAISE_SENSITIVITY = 0.5 127 | 128 | LOOK_SENSITIVITY = 0.01 129 | FREE_TURN_SENSITIVITY = 0.01 130 | 131 | dx = x - self.startx_ 132 | dy = y - self.starty_ 133 | 134 | if dx > 0.0: 135 | dx = max(0.0, dx - MIN_MOVE) 136 | if dx < 0.0: 137 | dx = min(0.0, dx + MIN_MOVE) 138 | if dy > 0.0: 139 | dy = max(0.0, dy - MIN_MOVE) 140 | if dy < 0.0: 141 | dy = min(0.0, dy + MIN_MOVE) 142 | 143 | # idea: if the velocity changes, we have to reset the start_time and update the camera parameters. 144 | 145 | if btn == glfw.MOUSE_BUTTON_RIGHT: 146 | 147 | self.forwardVel_ = 0 148 | self.upVel_ = 0 149 | self.sideVel_ = 0 150 | self.turnVel_ = 0 151 | 152 | self.yaw_ = self.startyaw_ - FREE_TURN_SENSITIVITY * dx 153 | self.pitch_ = self.startpitch_ - LOOK_SENSITIVITY * dy 154 | 155 | # ensure valid values. 156 | if self.pitch_ < -0.5 * math.pi: 157 | self.pitch_ = -0.5 * math.pi 158 | if self.pitch_ > 0.5 * math.pi: 159 | self.pitch_ = 0.5 * math.pi 160 | 161 | elif btn == glfw.MOUSE_BUTTON_LEFT: 162 | 163 | # apply transformation: 164 | end = time.time() 165 | dt = end - self.startTime_ 166 | 167 | if dt > 0.0: 168 | self.rotate(self.turnVel_ * dt, 0.0) 169 | self.translate(self.forwardVel_ * dt, self.upVel_ * dt, self.sideVel_ * dt) 170 | 171 | self.startTime_ = end 172 | # reset timer. 173 | 174 | self.forwardVel_ = -WALK_SENSITIVITY * dy 175 | self.upVel_ = 0 176 | self.sideVel_ = 0 177 | self.turnVel_ = -(TURN_SENSITIVITY * dx) 178 | elif btn == glfw.MOUSE_BUTTON_MIDDLE: 179 | 180 | # apply transformation: 181 | end = time.time() 182 | dt = end - self.startTime_ 183 | 184 | if dt > 0.0: 185 | self.rotate(self.turnVel_ * dt, 0.0) 186 | self.translate(self.forwardVel_ * dt, self.upVel_ * dt, self.sideVel_ * dt) 187 | 188 | self.startTime_ = end 189 | # reset timer. 190 | 191 | self.forwardVel_ = 0 192 | self.upVel_ = -RAISE_SENSITIVITY * dy 193 | self.sideVel_ = SLIDE_SENSITIVITY * dx 194 | self.turnVel_ = 0 195 | 196 | return True 197 | -------------------------------------------------------------------------------- /visualize_mos.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # This file is covered by the LICENSE file in the root of this project. 3 | # developed by Xieyuanli Chen 4 | 5 | import argparse 6 | import os 7 | import yaml 8 | from auxiliary.laserscan import LaserScan, SemLaserScan 9 | from auxiliary.laserscanvis import LaserScanVis 10 | 11 | if __name__ == '__main__': 12 | parser = argparse.ArgumentParser("./visualize.py") 13 | parser.add_argument( 14 | '--dataset', '-d', 15 | type=str, 16 | required=True, 17 | help='Dataset to visualize. No Default', 18 | ) 19 | parser.add_argument( 20 | '--config', '-c', 21 | type=str, 22 | required=False, 23 | default="config/semantic-kitti-mos.yaml", 24 | help='Dataset config file. Defaults to %(default)s', 25 | ) 26 | parser.add_argument( 27 | '--sequence', '-s', 28 | type=str, 29 | default="00", 30 | required=False, 31 | help='Sequence to visualize. Defaults to %(default)s', 32 | ) 33 | parser.add_argument( 34 | '--predictions', '-p', 35 | type=str, 36 | default=None, 37 | required=False, 38 | help='Alternate location for labels, to use predictions folder. ' 39 | 'Must point to directory containing the predictions in the proper format ' 40 | ' (see readme)' 41 | 'Defaults to %(default)s', 42 | ) 43 | parser.add_argument( 44 | '--ignore_semantics', '-i', 45 | dest='ignore_semantics', 46 | default=False, 47 | action='store_true', 48 | help='Ignore semantics. Visualizes uncolored pointclouds.' 49 | 'Defaults to %(default)s', 50 | ) 51 | parser.add_argument( 52 | '--do_instances', '-di', 53 | dest='do_instances', 54 | default=False, 55 | action='store_true', 56 | help='Visualize instances too. Defaults to %(default)s', 57 | ) 58 | parser.add_argument( 59 | '--offset', 60 | type=int, 61 | default=0, 62 | required=False, 63 | help='Sequence to start. Defaults to %(default)s', 64 | ) 65 | parser.add_argument( 66 | '--ignore_safety', 67 | dest='ignore_safety', 68 | default=False, 69 | action='store_true', 70 | help='Normally you want the number of labels and ptcls to be the same,' 71 | ', but if you are not done inferring this is not the case, so this disables' 72 | ' that safety.' 73 | 'Defaults to %(default)s', 74 | ) 75 | parser.add_argument( 76 | '--color_learning_map', 77 | dest='color_learning_map', 78 | default=False, 79 | required=False, 80 | action='store_true', 81 | help='Apply learning map to color map: visualize only classes that were trained on', 82 | ) 83 | FLAGS, unparsed = parser.parse_known_args() 84 | 85 | # print summary of what we will do 86 | print("*" * 80) 87 | print("INTERFACE:") 88 | print("Dataset", FLAGS.dataset) 89 | print("Config", FLAGS.config) 90 | print("Sequence", FLAGS.sequence) 91 | print("Predictions", FLAGS.predictions) 92 | print("ignore_semantics", FLAGS.ignore_semantics) 93 | print("do_instances", FLAGS.do_instances) 94 | print("ignore_safety", FLAGS.ignore_safety) 95 | print("color_learning_map", FLAGS.color_learning_map) 96 | print("offset", FLAGS.offset) 97 | print("*" * 80) 98 | 99 | # open config file 100 | try: 101 | print("Opening config file %s" % FLAGS.config) 102 | CFG = yaml.safe_load(open(FLAGS.config, 'r')) 103 | except Exception as e: 104 | print(e) 105 | print("Error opening yaml file.") 106 | quit() 107 | 108 | # fix sequence name 109 | FLAGS.sequence = '{0:02d}'.format(int(FLAGS.sequence)) 110 | 111 | # does sequence folder exist? 112 | scan_paths = os.path.join(FLAGS.dataset, "sequences", 113 | FLAGS.sequence, "velodyne") 114 | if os.path.isdir(scan_paths): 115 | print("Sequence folder exists! Using sequence from %s" % scan_paths) 116 | else: 117 | print("Sequence folder doesn't exist! Exiting...") 118 | quit() 119 | 120 | # populate the pointclouds 121 | scan_names = [os.path.join(dp, f) for dp, dn, fn in os.walk( 122 | os.path.expanduser(scan_paths)) for f in fn] 123 | scan_names.sort() 124 | 125 | # does sequence folder exist? 126 | if not FLAGS.ignore_semantics: 127 | if FLAGS.predictions is not None: 128 | label_paths = os.path.join(FLAGS.predictions, "sequences", 129 | FLAGS.sequence, "predictions") 130 | else: 131 | label_paths = os.path.join(FLAGS.dataset, "sequences", 132 | FLAGS.sequence, "labels") 133 | if os.path.isdir(label_paths): 134 | print("Labels folder exists! Using labels from %s" % label_paths) 135 | else: 136 | print(label_paths) 137 | print("Labels folder doesn't exist! Exiting...") 138 | quit() 139 | # populate the pointclouds 140 | label_names = [os.path.join(dp, f) for dp, dn, fn in os.walk( 141 | os.path.expanduser(label_paths)) for f in fn] 142 | label_names.sort() 143 | 144 | # check that there are same amount of labels and scans 145 | if not FLAGS.ignore_safety: 146 | assert(len(label_names) == len(scan_names)) 147 | 148 | # create a scan 149 | if FLAGS.ignore_semantics: 150 | scan = LaserScan(project=True) # project all opened scans to spheric proj 151 | else: 152 | color_dict = CFG["color_map"] 153 | if FLAGS.color_learning_map: 154 | learning_map_inv = CFG["learning_map_inv"] 155 | learning_map = CFG["learning_map"] 156 | color_dict = {key:color_dict[learning_map_inv[learning_map[key]]] for key, value in color_dict.items()} 157 | 158 | scan = SemLaserScan(color_dict, project=True) 159 | 160 | # create a visualizer 161 | semantics = not FLAGS.ignore_semantics 162 | instances = FLAGS.do_instances 163 | if not semantics: 164 | label_names = None 165 | vis = LaserScanVis(scan=scan, 166 | scan_names=scan_names, 167 | label_names=label_names, 168 | offset=FLAGS.offset, 169 | semantics=semantics, instances=instances and semantics) 170 | 171 | # print instructions 172 | print("To navigate:") 173 | print("\tb: back (previous scan)") 174 | print("\tn: next (next scan)") 175 | print("\tq: quit (exit program)") 176 | 177 | # run the visualizer 178 | vis.run() 179 | -------------------------------------------------------------------------------- /compare.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # This file is covered by the LICENSE file in the root of this project. 3 | 4 | import argparse 5 | import os 6 | import yaml 7 | from auxiliary.laserscan import LaserScan, SemLaserScan 8 | from auxiliary.laserscancomp import LaserScanComp 9 | 10 | if __name__ == '__main__': 11 | parser = argparse.ArgumentParser("./compare.py") 12 | parser.add_argument( 13 | '--dataset', '-d', 14 | type=str, 15 | required=True, 16 | help='Dataset to visualize. No Default', 17 | ) 18 | parser.add_argument( 19 | '--labels', 20 | required=True, 21 | nargs='+', 22 | help='Labels A to visualize. No Default', 23 | ) 24 | parser.add_argument( 25 | '--config', '-c', 26 | type=str, 27 | required=False, 28 | default="config/semantic-kitti.yaml", 29 | help='Dataset config file. Defaults to %(default)s', 30 | ) 31 | parser.add_argument( 32 | '--sequence', '-s', 33 | type=str, 34 | default="00", 35 | required=False, 36 | help='Sequence to visualize. Defaults to %(default)s', 37 | ) 38 | parser.add_argument( 39 | '--ignore_images', '-r', 40 | dest='ignore_images', 41 | default=False, 42 | required=False, 43 | action='store_true', 44 | help='Visualize range image projections too. Defaults to %(default)s', 45 | ) 46 | parser.add_argument( 47 | '--do_instances', '-i', 48 | dest='do_instances', 49 | default=False, 50 | required=False, 51 | action='store_true', 52 | help='Visualize instances too. Defaults to %(default)s', 53 | ) 54 | parser.add_argument( 55 | '--link', '-l', 56 | dest='link', 57 | default=False, 58 | required=False, 59 | action='store_true', 60 | help='Link viewpoint changes across windows. Defaults to %(default)s', 61 | ) 62 | parser.add_argument( 63 | '--offset', 64 | type=int, 65 | default=0, 66 | required=False, 67 | help='Sequence to start. Defaults to %(default)s', 68 | ) 69 | parser.add_argument( 70 | '--ignore_safety', 71 | dest='ignore_safety', 72 | default=False, 73 | required=False, 74 | action='store_true', 75 | help='Normally you want the number of labels and ptcls to be the same,' 76 | ', but if you are not done inferring this is not the case, so this disables' 77 | ' that safety.' 78 | 'Defaults to %(default)s', 79 | ) 80 | parser.add_argument( 81 | '--color_learning_map', 82 | dest='color_learning_map', 83 | default=False, 84 | required=False, 85 | action='store_true', 86 | help='Apply learning map to color map: visualize only classes that were trained on', 87 | ) 88 | FLAGS, unparsed = parser.parse_known_args() 89 | 90 | # print summary of what we will do 91 | print("*" * 80) 92 | print("INTERFACE:") 93 | print("Labels: ", FLAGS.labels) 94 | print("Config", FLAGS.config) 95 | print("Sequence", FLAGS.sequence) 96 | print("ignore_images", FLAGS.ignore_images) 97 | print("do_instances", FLAGS.do_instances) 98 | print("link", FLAGS.link) 99 | print("ignore_safety", FLAGS.ignore_safety) 100 | print("color_learning_map", FLAGS.color_learning_map) 101 | print("offset", FLAGS.offset) 102 | print("*" * 80) 103 | 104 | # open config file 105 | try: 106 | print("Opening config file %s" % FLAGS.config) 107 | CFG = yaml.safe_load(open(FLAGS.config, 'r')) 108 | except Exception as e: 109 | print(e) 110 | print("Error opening yaml file.") 111 | quit() 112 | 113 | # fix sequence name 114 | FLAGS.sequence = '{0:02d}'.format(int(FLAGS.sequence)) 115 | 116 | # does sequence folder exist? 117 | scan_paths = os.path.join(FLAGS.dataset, "sequences", FLAGS.sequence, "velodyne") 118 | 119 | if os.path.isdir(scan_paths): 120 | print("Sequence folder a exists! Using sequence from %s" % scan_paths) 121 | else: 122 | print(f"Sequence folder {scan_paths} doesn't exist! Exiting...") 123 | quit() 124 | 125 | # populate the pointclouds 126 | scan_names = [os.path.join(dp, f) for dp, dn, fn in os.walk(os.path.expanduser(scan_paths)) for f in fn] 127 | scan_names.sort() 128 | 129 | print(len(scan_names)) 130 | 131 | # does sequence folder exist? 132 | assert len(FLAGS.labels) == 2 133 | labels_a, labels_b = FLAGS.labels[0], FLAGS.labels[1] 134 | label_a_paths = os.path.join(FLAGS.dataset, "sequences", FLAGS.sequence, labels_a) 135 | label_b_paths = os.path.join(FLAGS.dataset, "sequences", FLAGS.sequence, labels_b) 136 | 137 | if os.path.isdir(label_a_paths): 138 | print("Labels folder a exists! Using labels from %s" % label_a_paths) 139 | else: 140 | print("Labels folder a doesn't exist! Exiting...") 141 | quit() 142 | 143 | if os.path.isdir(label_b_paths): 144 | print("Labels folder b exists! Using labels from %s" % label_b_paths) 145 | else: 146 | print("Labels folder b doesn't exist! Exiting...") 147 | quit() 148 | 149 | # populate the pointclouds 150 | label_a_names = [os.path.join(dp, f) for dp, dn, fn in os.walk(os.path.expanduser(label_a_paths)) for f in fn] 151 | label_a_names.sort() 152 | label_b_names = [os.path.join(dp, f) for dp, dn, fn in os.walk(os.path.expanduser(label_b_paths)) for f in fn] 153 | label_b_names.sort() 154 | 155 | # check that there are same amount of labels and scans 156 | if not FLAGS.ignore_safety: 157 | assert len(label_a_names) == len(scan_names) 158 | assert len(label_b_names) == len(scan_names) 159 | 160 | # create scans 161 | color_dict = CFG["color_map"] 162 | if FLAGS.color_learning_map: 163 | learning_map_inv = CFG["learning_map_inv"] 164 | learning_map = CFG["learning_map"] 165 | color_dict = {key: color_dict[learning_map_inv[learning_map[key]]] for key, value in color_dict.items()} 166 | 167 | scan_b = SemLaserScan(color_dict, project=True) 168 | scan_a = SemLaserScan(color_dict, project=True) 169 | 170 | # create a visualizer 171 | images = not FLAGS.ignore_images 172 | vis = LaserScanComp(scans=(scan_a, scan_b), 173 | scan_names=scan_names, 174 | label_names=(label_a_names, label_b_names), 175 | offset=FLAGS.offset, images=images, instances=FLAGS.do_instances, link=FLAGS.link) 176 | 177 | # print instructions 178 | print("To navigate:") 179 | print("\tb: back (previous scan)") 180 | print("\tn: next (next scan)") 181 | print("\tq: quit (exit program)") 182 | 183 | # run the visualizer 184 | vis.run() 185 | -------------------------------------------------------------------------------- /config/semantic-kitti.yaml: -------------------------------------------------------------------------------- 1 | # This file is covered by the LICENSE file in the root of this project. 2 | labels: 3 | 0 : "unlabeled" 4 | 1 : "outlier" 5 | 10: "car" 6 | 11: "bicycle" 7 | 13: "bus" 8 | 15: "motorcycle" 9 | 16: "on-rails" 10 | 18: "truck" 11 | 20: "other-vehicle" 12 | 30: "person" 13 | 31: "bicyclist" 14 | 32: "motorcyclist" 15 | 40: "road" 16 | 44: "parking" 17 | 48: "sidewalk" 18 | 49: "other-ground" 19 | 50: "building" 20 | 51: "fence" 21 | 52: "other-structure" 22 | 60: "lane-marking" 23 | 70: "vegetation" 24 | 71: "trunk" 25 | 72: "terrain" 26 | 80: "pole" 27 | 81: "traffic-sign" 28 | 99: "other-object" 29 | 252: "moving-car" 30 | 253: "moving-bicyclist" 31 | 254: "moving-person" 32 | 255: "moving-motorcyclist" 33 | 256: "moving-on-rails" 34 | 257: "moving-bus" 35 | 258: "moving-truck" 36 | 259: "moving-other-vehicle" 37 | color_map: # bgr 38 | 0 : [0, 0, 0] 39 | 1 : [0, 0, 255] 40 | 10: [245, 150, 100] 41 | 11: [245, 230, 100] 42 | 13: [250, 80, 100] 43 | 15: [150, 60, 30] 44 | 16: [255, 0, 0] 45 | 18: [180, 30, 80] 46 | 20: [255, 0, 0] 47 | 30: [30, 30, 255] 48 | 31: [200, 40, 255] 49 | 32: [90, 30, 150] 50 | 40: [255, 0, 255] 51 | 44: [255, 150, 255] 52 | 48: [75, 0, 75] 53 | 49: [75, 0, 175] 54 | 50: [0, 200, 255] 55 | 51: [50, 120, 255] 56 | 52: [0, 150, 255] 57 | 60: [170, 255, 150] 58 | 70: [0, 175, 0] 59 | 71: [0, 60, 135] 60 | 72: [80, 240, 150] 61 | 80: [150, 240, 255] 62 | 81: [0, 0, 255] 63 | 99: [255, 255, 50] 64 | 252: [245, 150, 100] 65 | 256: [255, 0, 0] 66 | 253: [200, 40, 255] 67 | 254: [30, 30, 255] 68 | 255: [90, 30, 150] 69 | 257: [250, 80, 100] 70 | 258: [180, 30, 80] 71 | 259: [255, 0, 0] 72 | content: # as a ratio with the total number of points 73 | 0: 0.018889854628292943 74 | 1: 0.0002937197336781505 75 | 10: 0.040818519255974316 76 | 11: 0.00016609538710764618 77 | 13: 2.7879693665067774e-05 78 | 15: 0.00039838616015114444 79 | 16: 0.0 80 | 18: 0.0020633612104619787 81 | 20: 0.0016218197275284021 82 | 30: 0.00017698551338515307 83 | 31: 1.1065903904919655e-08 84 | 32: 5.532951952459828e-09 85 | 40: 0.1987493871255525 86 | 44: 0.014717169549888214 87 | 48: 0.14392298360372 88 | 49: 0.0039048553037472045 89 | 50: 0.1326861944777486 90 | 51: 0.0723592229456223 91 | 52: 0.002395131480328884 92 | 60: 4.7084144280367186e-05 93 | 70: 0.26681502148037506 94 | 71: 0.006035012012626033 95 | 72: 0.07814222006271769 96 | 80: 0.002855498193863172 97 | 81: 0.0006155958086189918 98 | 99: 0.009923127583046915 99 | 252: 0.001789309418528068 100 | 253: 0.00012709999297008662 101 | 254: 0.00016059776092534436 102 | 255: 3.745553104802113e-05 103 | 256: 0.0 104 | 257: 0.00011351574470342043 105 | 258: 0.00010157861367183268 106 | 259: 4.3840131989471124e-05 107 | # classes that are indistinguishable from single scan or inconsistent in 108 | # ground truth are mapped to their closest equivalent 109 | learning_map: 110 | 0 : 0 # "unlabeled" 111 | 1 : 0 # "outlier" mapped to "unlabeled" --------------------------mapped 112 | 10: 1 # "car" 113 | 11: 2 # "bicycle" 114 | 13: 5 # "bus" mapped to "other-vehicle" --------------------------mapped 115 | 15: 3 # "motorcycle" 116 | 16: 5 # "on-rails" mapped to "other-vehicle" ---------------------mapped 117 | 18: 4 # "truck" 118 | 20: 5 # "other-vehicle" 119 | 30: 6 # "person" 120 | 31: 7 # "bicyclist" 121 | 32: 8 # "motorcyclist" 122 | 40: 9 # "road" 123 | 44: 10 # "parking" 124 | 48: 11 # "sidewalk" 125 | 49: 12 # "other-ground" 126 | 50: 13 # "building" 127 | 51: 14 # "fence" 128 | 52: 0 # "other-structure" mapped to "unlabeled" ------------------mapped 129 | 60: 9 # "lane-marking" to "road" ---------------------------------mapped 130 | 70: 15 # "vegetation" 131 | 71: 16 # "trunk" 132 | 72: 17 # "terrain" 133 | 80: 18 # "pole" 134 | 81: 19 # "traffic-sign" 135 | 99: 0 # "other-object" to "unlabeled" ----------------------------mapped 136 | 252: 1 # "moving-car" to "car" ------------------------------------mapped 137 | 253: 7 # "moving-bicyclist" to "bicyclist" ------------------------mapped 138 | 254: 6 # "moving-person" to "person" ------------------------------mapped 139 | 255: 8 # "moving-motorcyclist" to "motorcyclist" ------------------mapped 140 | 256: 5 # "moving-on-rails" mapped to "other-vehicle" --------------mapped 141 | 257: 5 # "moving-bus" mapped to "other-vehicle" -------------------mapped 142 | 258: 4 # "moving-truck" to "truck" --------------------------------mapped 143 | 259: 5 # "moving-other"-vehicle to "other-vehicle" ----------------mapped 144 | learning_map_inv: # inverse of previous map 145 | 0: 0 # "unlabeled", and others ignored 146 | 1: 10 # "car" 147 | 2: 11 # "bicycle" 148 | 3: 15 # "motorcycle" 149 | 4: 18 # "truck" 150 | 5: 20 # "other-vehicle" 151 | 6: 30 # "person" 152 | 7: 31 # "bicyclist" 153 | 8: 32 # "motorcyclist" 154 | 9: 40 # "road" 155 | 10: 44 # "parking" 156 | 11: 48 # "sidewalk" 157 | 12: 49 # "other-ground" 158 | 13: 50 # "building" 159 | 14: 51 # "fence" 160 | 15: 70 # "vegetation" 161 | 16: 71 # "trunk" 162 | 17: 72 # "terrain" 163 | 18: 80 # "pole" 164 | 19: 81 # "traffic-sign" 165 | learning_ignore: # Ignore classes 166 | 0: True # "unlabeled", and others ignored 167 | 1: False # "car" 168 | 2: False # "bicycle" 169 | 3: False # "motorcycle" 170 | 4: False # "truck" 171 | 5: False # "other-vehicle" 172 | 6: False # "person" 173 | 7: False # "bicyclist" 174 | 8: False # "motorcyclist" 175 | 9: False # "road" 176 | 10: False # "parking" 177 | 11: False # "sidewalk" 178 | 12: False # "other-ground" 179 | 13: False # "building" 180 | 14: False # "fence" 181 | 15: False # "vegetation" 182 | 16: False # "trunk" 183 | 17: False # "terrain" 184 | 18: False # "pole" 185 | 19: False # "traffic-sign" 186 | split: # sequence numbers 187 | train: 188 | - 0 189 | - 1 190 | - 2 191 | - 3 192 | - 4 193 | - 5 194 | - 6 195 | - 7 196 | - 9 197 | - 10 198 | valid: 199 | - 8 200 | test: 201 | - 11 202 | - 12 203 | - 13 204 | - 14 205 | - 15 206 | - 16 207 | - 17 208 | - 18 209 | - 19 210 | - 20 211 | - 21 212 | -------------------------------------------------------------------------------- /config/semantic-kitti-all.yaml: -------------------------------------------------------------------------------- 1 | # This file is covered by the LICENSE file in the root of this project. 2 | labels: 3 | 0 : "unlabeled" 4 | 1 : "outlier" 5 | 10: "car" 6 | 11: "bicycle" 7 | 13: "bus" 8 | 15: "motorcycle" 9 | 16: "on-rails" 10 | 18: "truck" 11 | 20: "other-vehicle" 12 | 30: "person" 13 | 31: "bicyclist" 14 | 32: "motorcyclist" 15 | 40: "road" 16 | 44: "parking" 17 | 48: "sidewalk" 18 | 49: "other-ground" 19 | 50: "building" 20 | 51: "fence" 21 | 52: "other-structure" 22 | 60: "lane-marking" 23 | 70: "vegetation" 24 | 71: "trunk" 25 | 72: "terrain" 26 | 80: "pole" 27 | 81: "traffic-sign" 28 | 99: "other-object" 29 | 252: "moving-car" 30 | 253: "moving-bicyclist" 31 | 254: "moving-person" 32 | 255: "moving-motorcyclist" 33 | 256: "moving-on-rails" 34 | 257: "moving-bus" 35 | 258: "moving-truck" 36 | 259: "moving-other-vehicle" 37 | color_map: # bgr 38 | 0 : [0, 0, 0] 39 | 1 : [0, 0, 255] 40 | 10: [245, 150, 100] 41 | 11: [245, 230, 100] 42 | 13: [250, 80, 100] 43 | 15: [150, 60, 30] 44 | 16: [255, 0, 0] 45 | 18: [180, 30, 80] 46 | 20: [255, 0, 0] 47 | 30: [30, 30, 255] 48 | 31: [200, 40, 255] 49 | 32: [90, 30, 150] 50 | 40: [255, 0, 255] 51 | 44: [255, 150, 255] 52 | 48: [75, 0, 75] 53 | 49: [75, 0, 175] 54 | 50: [0, 200, 255] 55 | 51: [50, 120, 255] 56 | 52: [0, 150, 255] 57 | 60: [170, 255, 150] 58 | 70: [0, 175, 0] 59 | 71: [0, 60, 135] 60 | 72: [80, 240, 150] 61 | 80: [150, 240, 255] 62 | 81: [0, 0, 255] 63 | 99: [255, 255, 50] 64 | 252: [245, 150, 100] 65 | 256: [255, 0, 0] 66 | 253: [200, 40, 255] 67 | 254: [30, 30, 255] 68 | 255: [90, 30, 150] 69 | 257: [250, 80, 100] 70 | 258: [180, 30, 80] 71 | 259: [255, 0, 0] 72 | content: # as a ratio with the total number of points 73 | 0: 0.018889854628292943 74 | 1: 0.0002937197336781505 75 | 10: 0.040818519255974316 76 | 11: 0.00016609538710764618 77 | 13: 2.7879693665067774e-05 78 | 15: 0.00039838616015114444 79 | 16: 0.0 80 | 18: 0.0020633612104619787 81 | 20: 0.0016218197275284021 82 | 30: 0.00017698551338515307 83 | 31: 1.1065903904919655e-08 84 | 32: 5.532951952459828e-09 85 | 40: 0.1987493871255525 86 | 44: 0.014717169549888214 87 | 48: 0.14392298360372 88 | 49: 0.0039048553037472045 89 | 50: 0.1326861944777486 90 | 51: 0.0723592229456223 91 | 52: 0.002395131480328884 92 | 60: 4.7084144280367186e-05 93 | 70: 0.26681502148037506 94 | 71: 0.006035012012626033 95 | 72: 0.07814222006271769 96 | 80: 0.002855498193863172 97 | 81: 0.0006155958086189918 98 | 99: 0.009923127583046915 99 | 252: 0.001789309418528068 100 | 253: 0.00012709999297008662 101 | 254: 0.00016059776092534436 102 | 255: 3.745553104802113e-05 103 | 256: 0.0 104 | 257: 0.00011351574470342043 105 | 258: 0.00010157861367183268 106 | 259: 4.3840131989471124e-05 107 | # classes that are indistinguishable from single scan or inconsistent in 108 | # ground truth are mapped to their closest equivalent 109 | learning_map: 110 | 0 : 0 # "unlabeled" 111 | 1 : 0 # "outlier" mapped to "unlabeled" --------------------------mapped 112 | 10: 1 # "car" 113 | 11: 2 # "bicycle" 114 | 13: 5 # "bus" mapped to "other-vehicle" --------------------------mapped 115 | 15: 3 # "motorcycle" 116 | 16: 5 # "on-rails" mapped to "other-vehicle" ---------------------mapped 117 | 18: 4 # "truck" 118 | 20: 5 # "other-vehicle" 119 | 30: 6 # "person" 120 | 31: 7 # "bicyclist" 121 | 32: 8 # "motorcyclist" 122 | 40: 9 # "road" 123 | 44: 10 # "parking" 124 | 48: 11 # "sidewalk" 125 | 49: 12 # "other-ground" 126 | 50: 13 # "building" 127 | 51: 14 # "fence" 128 | 52: 0 # "other-structure" mapped to "unlabeled" ------------------mapped 129 | 60: 9 # "lane-marking" to "road" ---------------------------------mapped 130 | 70: 15 # "vegetation" 131 | 71: 16 # "trunk" 132 | 72: 17 # "terrain" 133 | 80: 18 # "pole" 134 | 81: 19 # "traffic-sign" 135 | 99: 0 # "other-object" to "unlabeled" ----------------------------mapped 136 | 252: 20 # "moving-car" 137 | 253: 21 # "moving-bicyclist" 138 | 254: 22 # "moving-person" 139 | 255: 23 # "moving-motorcyclist" 140 | 256: 24 # "moving-on-rails" mapped to "moving-other-vehicle" ------mapped 141 | 257: 24 # "moving-bus" mapped to "moving-other-vehicle" -----------mapped 142 | 258: 25 # "moving-truck" 143 | 259: 24 # "moving-other-vehicle" 144 | learning_map_inv: # inverse of previous map 145 | 0: 0 # "unlabeled", and others ignored 146 | 1: 10 # "car" 147 | 2: 11 # "bicycle" 148 | 3: 15 # "motorcycle" 149 | 4: 18 # "truck" 150 | 5: 20 # "other-vehicle" 151 | 6: 30 # "person" 152 | 7: 31 # "bicyclist" 153 | 8: 32 # "motorcyclist" 154 | 9: 40 # "road" 155 | 10: 44 # "parking" 156 | 11: 48 # "sidewalk" 157 | 12: 49 # "other-ground" 158 | 13: 50 # "building" 159 | 14: 51 # "fence" 160 | 15: 70 # "vegetation" 161 | 16: 71 # "trunk" 162 | 17: 72 # "terrain" 163 | 18: 80 # "pole" 164 | 19: 81 # "traffic-sign" 165 | 20: 252 # "moving-car" 166 | 21: 253 # "moving-bicyclist" 167 | 22: 254 # "moving-person" 168 | 23: 255 # "moving-motorcyclist" 169 | 24: 259 # "moving-other-vehicle" 170 | 25: 258 # "moving-truck" 171 | learning_ignore: # Ignore classes 172 | 0: True # "unlabeled", and others ignored 173 | 1: False # "car" 174 | 2: False # "bicycle" 175 | 3: False # "motorcycle" 176 | 4: False # "truck" 177 | 5: False # "other-vehicle" 178 | 6: False # "person" 179 | 7: False # "bicyclist" 180 | 8: False # "motorcyclist" 181 | 9: False # "road" 182 | 10: False # "parking" 183 | 11: False # "sidewalk" 184 | 12: False # "other-ground" 185 | 13: False # "building" 186 | 14: False # "fence" 187 | 15: False # "vegetation" 188 | 16: False # "trunk" 189 | 17: False # "terrain" 190 | 18: False # "pole" 191 | 19: False # "traffic-sign" 192 | 20: False # "moving-car" 193 | 21: False # "moving-bicyclist" 194 | 22: False # "moving-person" 195 | 23: False # "moving-motorcyclist" 196 | 24: False # "moving-other-vehicle" 197 | 25: False # "moving-truck" 198 | split: # sequence numbers 199 | train: 200 | - 0 201 | - 1 202 | - 2 203 | - 3 204 | - 4 205 | - 5 206 | - 6 207 | - 7 208 | - 9 209 | - 10 210 | valid: 211 | - 8 212 | test: 213 | - 11 214 | - 12 215 | - 13 216 | - 14 217 | - 15 218 | - 16 219 | - 17 220 | - 18 221 | - 19 222 | - 20 223 | - 21 224 | -------------------------------------------------------------------------------- /visualize.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # This file is covered by the LICENSE file in the root of this project. 3 | 4 | import argparse 5 | import os 6 | import yaml 7 | from auxiliary.laserscan import LaserScan, SemLaserScan 8 | from auxiliary.laserscanvis import LaserScanVis 9 | 10 | if __name__ == '__main__': 11 | parser = argparse.ArgumentParser("./visualize.py") 12 | parser.add_argument( 13 | '--dataset', '-d', 14 | type=str, 15 | required=True, 16 | help='Dataset to visualize. No Default', 17 | ) 18 | parser.add_argument( 19 | '--config', '-c', 20 | type=str, 21 | required=False, 22 | default="config/semantic-kitti.yaml", 23 | help='Dataset config file. Defaults to %(default)s', 24 | ) 25 | parser.add_argument( 26 | '--sequence', '-s', 27 | type=str, 28 | default="00", 29 | required=False, 30 | help='Sequence to visualize. Defaults to %(default)s', 31 | ) 32 | parser.add_argument( 33 | '--predictions', '-p', 34 | type=str, 35 | default=None, 36 | required=False, 37 | help='Alternate location for labels, to use predictions folder. ' 38 | 'Must point to directory containing the predictions in the proper format ' 39 | ' (see readme)' 40 | 'Defaults to %(default)s', 41 | ) 42 | parser.add_argument( 43 | '--ignore_semantics', '-i', 44 | dest='ignore_semantics', 45 | default=False, 46 | action='store_true', 47 | help='Ignore semantics. Visualizes uncolored pointclouds.' 48 | 'Defaults to %(default)s', 49 | ) 50 | parser.add_argument( 51 | '--do_instances', '-o', 52 | dest='do_instances', 53 | default=False, 54 | required=False, 55 | action='store_true', 56 | help='Visualize instances too. Defaults to %(default)s', 57 | ) 58 | parser.add_argument( 59 | '--ignore_images', '-r', 60 | dest='ignore_images', 61 | default=False, 62 | required=False, 63 | action='store_true', 64 | help='Visualize range image projections too. Defaults to %(default)s', 65 | ) 66 | parser.add_argument( 67 | '--link', '-l', 68 | dest='link', 69 | default=False, 70 | required=False, 71 | action='store_true', 72 | help='Link viewpoint changes across windows. Defaults to %(default)s', 73 | ) 74 | parser.add_argument( 75 | '--offset', 76 | type=int, 77 | default=0, 78 | required=False, 79 | help='Sequence to start. Defaults to %(default)s', 80 | ) 81 | parser.add_argument( 82 | '--ignore_safety', 83 | dest='ignore_safety', 84 | default=False, 85 | required=False, 86 | action='store_true', 87 | help='Normally you want the number of labels and ptcls to be the same,' 88 | ', but if you are not done inferring this is not the case, so this disables' 89 | ' that safety.' 90 | 'Defaults to %(default)s', 91 | ) 92 | parser.add_argument( 93 | '--color_learning_map', 94 | dest='color_learning_map', 95 | default=False, 96 | required=False, 97 | action='store_true', 98 | help='Apply learning map to color map: visualize only classes that were trained on', 99 | ) 100 | FLAGS, unparsed = parser.parse_known_args() 101 | 102 | # print summary of what we will do 103 | print("*" * 80) 104 | print("INTERFACE:") 105 | print("Dataset", FLAGS.dataset) 106 | print("Config", FLAGS.config) 107 | print("Sequence", FLAGS.sequence) 108 | print("Predictions", FLAGS.predictions) 109 | print("ignore_semantics", FLAGS.ignore_semantics) 110 | print("do_instances", FLAGS.do_instances) 111 | print("ignore_images", FLAGS.ignore_images) 112 | print("link", FLAGS.link) 113 | print("ignore_safety", FLAGS.ignore_safety) 114 | print("color_learning_map", FLAGS.color_learning_map) 115 | print("offset", FLAGS.offset) 116 | print("*" * 80) 117 | 118 | # open config file 119 | try: 120 | print("Opening config file %s" % FLAGS.config) 121 | CFG = yaml.safe_load(open(FLAGS.config, 'r')) 122 | except Exception as e: 123 | print(e) 124 | print("Error opening yaml file.") 125 | quit() 126 | 127 | # fix sequence name 128 | FLAGS.sequence = '{0:02d}'.format(int(FLAGS.sequence)) 129 | 130 | # does sequence folder exist? 131 | scan_paths = os.path.join(FLAGS.dataset, "sequences", 132 | FLAGS.sequence, "velodyne") 133 | if os.path.isdir(scan_paths): 134 | print(f"Sequence folder {scan_paths} exists! Using sequence from {scan_paths}") 135 | else: 136 | print(f"Sequence folder {scan_paths} doesn't exist! Exiting...") 137 | quit() 138 | 139 | # populate the pointclouds 140 | scan_names = [os.path.join(dp, f) for dp, dn, fn in os.walk( 141 | os.path.expanduser(scan_paths)) for f in fn] 142 | scan_names.sort() 143 | 144 | # does sequence folder exist? 145 | if not FLAGS.ignore_semantics: 146 | if FLAGS.predictions is not None: 147 | label_paths = os.path.join(FLAGS.predictions, "sequences", 148 | FLAGS.sequence, "predictions") 149 | else: 150 | label_paths = os.path.join(FLAGS.dataset, "sequences", 151 | FLAGS.sequence, "labels") 152 | if os.path.isdir(label_paths): 153 | print(f"Labels folder {label_paths} exists! Using labels from {label_paths}") 154 | else: 155 | print(f"Labels folder {label_paths} doesn't exist! Exiting...") 156 | quit() 157 | 158 | # populate the pointclouds 159 | label_names = [os.path.join(dp, f) for dp, dn, fn in os.walk( 160 | os.path.expanduser(label_paths)) for f in fn] 161 | label_names.sort() 162 | 163 | # check that there are same amount of labels and scans 164 | if not FLAGS.ignore_safety: 165 | assert(len(label_names) == len(scan_names)) 166 | 167 | # create a scan 168 | if FLAGS.ignore_semantics: 169 | scan = LaserScan(project=True) # project all opened scans to spheric proj 170 | else: 171 | color_dict = CFG["color_map"] 172 | if FLAGS.color_learning_map: 173 | learning_map_inv = CFG["learning_map_inv"] 174 | learning_map = CFG["learning_map"] 175 | color_dict = {key:color_dict[learning_map_inv[learning_map[key]]] for key, value in color_dict.items()} 176 | 177 | scan = SemLaserScan(color_dict, project=True) 178 | 179 | # create a visualizer 180 | semantics = not FLAGS.ignore_semantics 181 | instances = FLAGS.do_instances 182 | images = not FLAGS.ignore_images 183 | if not semantics: 184 | label_names = None 185 | vis = LaserScanVis(scan=scan, 186 | scan_names=scan_names, 187 | label_names=label_names, 188 | offset=FLAGS.offset, 189 | semantics=semantics, instances=instances and semantics, images=images, link=FLAGS.link) 190 | 191 | # print instructions 192 | print("To navigate:") 193 | print("\tb: back (previous scan)") 194 | print("\tn: next (next scan)") 195 | print("\tq: quit (exit program)") 196 | 197 | # run the visualizer 198 | vis.run() 199 | -------------------------------------------------------------------------------- /generate_sequential.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # This file is covered by the LICENSE file in the root of this project. 3 | 4 | import argparse 5 | import os 6 | import yaml 7 | import numpy as np 8 | from collections import deque 9 | import shutil 10 | from numpy.linalg import inv 11 | import struct 12 | import time 13 | 14 | def parse_calibration(filename): 15 | """ read calibration file with given filename 16 | 17 | Returns 18 | ------- 19 | dict 20 | Calibration matrices as 4x4 numpy arrays. 21 | """ 22 | calib = {} 23 | 24 | calib_file = open(filename) 25 | for line in calib_file: 26 | key, content = line.strip().split(":") 27 | values = [float(v) for v in content.strip().split()] 28 | 29 | pose = np.zeros((4, 4)) 30 | pose[0, 0:4] = values[0:4] 31 | pose[1, 0:4] = values[4:8] 32 | pose[2, 0:4] = values[8:12] 33 | pose[3, 3] = 1.0 34 | 35 | calib[key] = pose 36 | 37 | calib_file.close() 38 | 39 | return calib 40 | 41 | 42 | def parse_poses(filename, calibration): 43 | """ read poses file with per-scan poses from given filename 44 | 45 | Returns 46 | ------- 47 | list 48 | list of poses as 4x4 numpy arrays. 49 | """ 50 | file = open(filename) 51 | 52 | poses = [] 53 | 54 | Tr = calibration["Tr"] 55 | Tr_inv = inv(Tr) 56 | 57 | for line in file: 58 | values = [float(v) for v in line.strip().split()] 59 | 60 | pose = np.zeros((4, 4)) 61 | pose[0, 0:4] = values[0:4] 62 | pose[1, 0:4] = values[4:8] 63 | pose[2, 0:4] = values[8:12] 64 | pose[3, 3] = 1.0 65 | 66 | poses.append(np.matmul(Tr_inv, np.matmul(pose, Tr))) 67 | 68 | return poses 69 | 70 | 71 | if __name__ == '__main__': 72 | start_time = time.time() 73 | 74 | parser = argparse.ArgumentParser("./generate_sequential.py") 75 | parser.add_argument( 76 | '--dataset', 77 | '-d', 78 | type=str, 79 | required=True, 80 | help='dataset folder containing all sequences in a folder called "sequences".', 81 | ) 82 | 83 | parser.add_argument( 84 | '--output', 85 | '-o', 86 | type=str, 87 | required=True, 88 | help='output folder for generated sequence scans.', 89 | ) 90 | 91 | parser.add_argument( 92 | '--sequence_length', 93 | '-s', 94 | type=int, 95 | required=True, 96 | help='length of sequence, i.e., how many scans are concatenated.', 97 | ) 98 | 99 | FLAGS, unparsed = parser.parse_known_args() 100 | 101 | # print summary of what we will do 102 | print("*" * 80) 103 | print(" dataset folder: ", FLAGS.dataset) 104 | print(" output folder: ", FLAGS.output) 105 | print("sequence length: ", FLAGS.sequence_length) 106 | print("*" * 80) 107 | 108 | sequences_dir = os.path.join(FLAGS.dataset, "sequences") 109 | sequence_folders = [ 110 | f for f in sorted(os.listdir(sequences_dir)) 111 | if os.path.isdir(os.path.join(sequences_dir, f)) 112 | ] 113 | 114 | for folder in sequence_folders: 115 | input_folder = os.path.join(sequences_dir, folder) 116 | output_folder = os.path.join(FLAGS.output, "sequences", folder) 117 | velodyne_folder = os.path.join(output_folder, "velodyne") 118 | labels_folder = os.path.join(output_folder, "labels") 119 | 120 | if os.path.exists(output_folder) or os.path.exists( 121 | velodyne_folder) or os.path.exists(labels_folder): 122 | print("Output folder '{}' already exists!".format(output_folder)) 123 | answer = input("Overwrite? [y/N] ") 124 | if answer != "y": 125 | print("Aborted.") 126 | exit(1) 127 | if not os.path.exists(velodyne_folder): 128 | os.makedirs(velodyne_folder) 129 | if not os.path.exists(labels_folder): 130 | os.makedirs(labels_folder) 131 | else: 132 | os.makedirs(velodyne_folder) 133 | os.makedirs(labels_folder) 134 | 135 | shutil.copy(os.path.join(input_folder, "poses.txt"), output_folder) 136 | shutil.copy(os.path.join(input_folder, "calib.txt"), output_folder) 137 | 138 | scan_files = [ 139 | f for f in sorted(os.listdir(os.path.join(input_folder, "velodyne"))) 140 | if f.endswith(".bin") 141 | ] 142 | 143 | history = deque() 144 | 145 | calibration = parse_calibration(os.path.join(input_folder, "calib.txt")) 146 | poses = parse_poses(os.path.join(input_folder, "poses.txt"), calibration) 147 | 148 | progress = 10 149 | 150 | print("Processing {} ".format(folder), end="", flush=True) 151 | 152 | for i, f in enumerate(scan_files): 153 | # read scan and labels, get pose 154 | scan_filename = os.path.join(input_folder, "velodyne", f) 155 | scan = np.fromfile(scan_filename, dtype=np.float32) 156 | 157 | scan = scan.reshape((-1, 4)) 158 | 159 | label_filename = os.path.join(input_folder, "labels", os.path.splitext(f)[0] + ".label") 160 | labels = np.fromfile(label_filename, dtype=np.uint32) 161 | labels = labels.reshape((-1)) 162 | 163 | # convert points to homogenous coordinates (x, y, z, 1) 164 | points = np.ones((scan.shape)) 165 | points[:, 0:3] = scan[:, 0:3] 166 | remissions = scan[:, 3] 167 | 168 | pose = poses[i] 169 | 170 | # prepare single numpy array for all points that can be written at once. 171 | num_concat_points = points.shape[0] 172 | num_concat_points += sum([past["points"].shape[0] for past in history]) 173 | concated_points = np.zeros((num_concat_points * 4), dtype = np.float32) 174 | concated_labels = np.zeros((num_concat_points), dtype = np.uint32) 175 | 176 | start = 0 177 | concated_points[4 * start:4 * (start + points.shape[0])] = scan.reshape((-1)) 178 | concated_labels[start:start + points.shape[0]] = labels 179 | start += points.shape[0] 180 | 181 | for past in history: 182 | diff = np.matmul(inv(pose), past["pose"]) 183 | tpoints = np.matmul(diff, past["points"].T).T 184 | tpoints[:, 3] = past["remissions"] 185 | tpoints = tpoints.reshape((-1)) 186 | 187 | concated_points[4 * start:4 * (start + past["points"].shape[0])] = tpoints 188 | concated_labels[start:start + past["labels"].shape[0]] = past["labels"] 189 | start += past["points"].shape[0] 190 | 191 | 192 | # write scan and labels in one pass. 193 | concated_points.tofile(os.path.join(velodyne_folder, f)) 194 | concated_labels.tofile(os.path.join(labels_folder, os.path.splitext(f)[0] + ".label")) 195 | 196 | # append current data to history queue. 197 | history.appendleft({ 198 | "points": points, 199 | "labels": labels, 200 | "remissions": remissions, 201 | "pose": pose.copy() 202 | }) 203 | 204 | if len(history) >= FLAGS.sequence_length: 205 | history.pop() 206 | 207 | if 100.0 * i / len(scan_files) >= progress: 208 | print(".", end="", flush=True) 209 | progress = progress + 10 210 | print("finished.") 211 | 212 | 213 | print("execution time: {}".format(time.time() - start_time)) 214 | -------------------------------------------------------------------------------- /config/semantic-kitti-mos.yaml: -------------------------------------------------------------------------------- 1 | # This file is covered by the LICENSE file in the root of this project. 2 | # developed by Xieyuanli Chen 3 | 4 | name: "kitti" 5 | labels: 6 | 0 : "unlabeled" 7 | 1 : "outlier" 8 | 9 : "static" # for lidar-mos static 9 | 10: "car" 10 | 11: "bicycle" 11 | 13: "bus" 12 | 15: "motorcycle" 13 | 16: "on-rails" 14 | 18: "truck" 15 | 20: "other-vehicle" 16 | 30: "person" 17 | 31: "bicyclist" 18 | 32: "motorcyclist" 19 | 40: "road" 20 | 44: "parking" 21 | 48: "sidewalk" 22 | 49: "other-ground" 23 | 50: "building" 24 | 51: "fence" 25 | 52: "other-structure" 26 | 60: "lane-marking" 27 | 70: "vegetation" 28 | 71: "trunk" 29 | 72: "terrain" 30 | 80: "pole" 31 | 81: "traffic-sign" 32 | 99: "other-object" 33 | 251: "moving" # lidar-mos mod moving 34 | 252: "moving-car" 35 | 253: "moving-bicyclist" 36 | 254: "moving-person" 37 | 255: "moving-motorcyclist" 38 | 256: "moving-on-rails" 39 | 257: "moving-bus" 40 | 258: "moving-truck" 41 | 259: "moving-other-vehicle" 42 | color_map: # bgr 43 | 0 : [0, 0, 0] 44 | 1 : [0, 0, 0] # [0, 0, 255] 45 | 9 : [0, 0, 0] # for lidar-mos static 46 | 10: [245, 150, 100] 47 | 11: [245, 230, 100] 48 | 13: [250, 80, 100] 49 | 15: [150, 60, 30] 50 | 16: [255, 0, 0] 51 | 18: [180, 30, 80] 52 | 20: [255, 0, 0] 53 | 30: [30, 30, 255] 54 | 31: [200, 40, 255] 55 | 32: [90, 30, 150] 56 | 40: [255, 0, 255] 57 | 44: [255, 150, 255] 58 | 48: [75, 0, 75] 59 | 49: [75, 0, 175] 60 | 50: [0, 200, 255] 61 | 51: [50, 120, 255] 62 | 52: [0, 150, 255] 63 | 60: [170, 255, 150] 64 | 70: [0, 175, 0] 65 | 71: [0, 60, 135] 66 | 72: [80, 240, 150] 67 | 80: [150, 240, 255] 68 | 81: [0, 0, 255] 69 | 99: [255, 255, 50] 70 | 251: [0, 0, 255] # lidar-mos mod moving 71 | 252: [245, 150, 100] 72 | 256: [255, 0, 0] 73 | 253: [200, 40, 255] 74 | 254: [30, 30, 255] 75 | 255: [90, 30, 150] 76 | 257: [250, 80, 100] 77 | 258: [180, 30, 80] 78 | 259: [255, 0, 0] 79 | content: # as a ratio with the total number of points 80 | 0: 0.018889854628292943 81 | 1: 0.0002937197336781505 82 | 10: 0.040818519255974316 83 | 11: 0.00016609538710764618 84 | 13: 2.7879693665067774e-05 85 | 15: 0.00039838616015114444 86 | 16: 0.0 87 | 18: 0.0020633612104619787 88 | 20: 0.0016218197275284021 89 | 30: 0.00017698551338515307 90 | 31: 1.1065903904919655e-08 91 | 32: 5.532951952459828e-09 92 | 40: 0.1987493871255525 93 | 44: 0.014717169549888214 94 | 48: 0.14392298360372 95 | 49: 0.0039048553037472045 96 | 50: 0.1326861944777486 97 | 51: 0.0723592229456223 98 | 52: 0.002395131480328884 99 | 60: 4.7084144280367186e-05 100 | 70: 0.26681502148037506 101 | 71: 0.006035012012626033 102 | 72: 0.07814222006271769 103 | 80: 0.002855498193863172 104 | 81: 0.0006155958086189918 105 | 99: 0.009923127583046915 106 | 252: 0.001789309418528068 107 | 253: 0.00012709999297008662 108 | 254: 0.00016059776092534436 109 | 255: 3.745553104802113e-05 110 | 256: 0.0 111 | 257: 0.00011351574470342043 112 | 258: 0.00010157861367183268 113 | 259: 4.3840131989471124e-05 114 | # classes that are indistinguishable from single scan or inconsistent in 115 | # ground truth are mapped to their closest equivalent 116 | learning_map: 117 | 0 : 0 # "unlabeled" mapped to "unlabeled" ------------------------mapped 118 | 1 : 0 # "outlier" mapped to "unlabeled" ------------------------mapped 119 | 9 : 1 # "static" mapped to "static" ---------------------------mapped 120 | 10: 1 # "car" mapped to "static" ---------------------------mapped 121 | 11: 1 # "bicycle" mapped to "static" ---------------------------mapped 122 | 13: 1 # "bus" mapped to "static" ---------------------------mapped 123 | 15: 1 # "motorcycle" mapped to "static" ---------------------------mapped 124 | 16: 1 # "on-rails" mapped to "static" ---------------------------mapped 125 | 18: 1 # "truck" mapped to "static" ---------------------------mapped 126 | 20: 1 # "other-vehicle" mapped to "static" ---------------------------mapped 127 | 30: 1 # "person" mapped to "static" ---------------------------mapped 128 | 31: 1 # "bicyclist" mapped to "static" ---------------------------mapped 129 | 32: 1 # "motorcyclist" mapped to "static" ---------------------------mapped 130 | 40: 1 # "road" mapped to "static" ---------------------------mapped 131 | 44: 1 # "parking" mapped to "static" ---------------------------mapped 132 | 48: 1 # "sidewalk" mapped to "static" ---------------------------mapped 133 | 49: 1 # "other-ground" mapped to "static" ---------------------------mapped 134 | 50: 1 # "building" mapped to "static" ---------------------------mapped 135 | 51: 1 # "fence" mapped to "static" ---------------------------mapped 136 | 52: 1 # "other-structure" mapped to "static" ---------------------------mapped 137 | 60: 1 # "lane-marking" mapped to "static" ---------------------------mapped 138 | 70: 1 # "vegetation" mapped to "static" ---------------------------mapped 139 | 71: 1 # "trunk" mapped to "static" ---------------------------mapped 140 | 72: 1 # "terrain" mapped to "static" ---------------------------mapped 141 | 80: 1 # "pole" mapped to "static" ---------------------------mapped 142 | 81: 1 # "traffic-sign" mapped to "static" ---------------------------mapped 143 | 99: 1 # "other-object" mapped to "static" ---------------------------mapped 144 | 251: 2 # "moving" mapped to "moving" ---------------------------mapped 145 | 252: 2 # "moving-car" mapped to "moving" ---------------------------mapped 146 | 253: 2 # "moving-bicyclist" mapped to "moving" ---------------------------mapped 147 | 254: 2 # "moving-person" mapped to "moving" ---------------------------mapped 148 | 255: 2 # "moving-motorcyclist" mapped to "moving" ---------------------------mapped 149 | 256: 2 # "moving-on-rails" mapped to "moving" ---------------------------mapped 150 | 257: 2 # "moving-bus" mapped to "moving" ---------------------------mapped 151 | 258: 2 # "moving-truck" mapped to "moving" ---------------------------mapped 152 | 259: 2 # "moving-other" mapped to "moving" ---------------------------mapped 153 | learning_map_inv: # inverse of previous map 154 | 0: 0 # "unlabeled", and others ignored 155 | 1: 9 # "static" 156 | 2: 251 # "moving" 157 | 158 | learning_ignore: # Ignore classes 159 | 0: True # "unlabeled", and others ignored 160 | 1: False # "static" 161 | 2: False # "moving" 162 | 163 | split: # sequence numbers 164 | train: 165 | - 0 166 | - 1 167 | - 2 168 | - 3 169 | - 4 170 | - 5 171 | - 6 172 | - 7 173 | - 9 174 | - 10 175 | valid: 176 | - 8 177 | test: 178 | - 11 179 | - 12 180 | - 13 181 | - 14 182 | - 15 183 | - 16 184 | - 17 185 | - 18 186 | - 19 187 | - 20 188 | - 21 189 | -------------------------------------------------------------------------------- /evaluate_mos.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # This file is covered by the LICENSE file in the root of this project. 3 | # developed by Xieyuanli Chen 4 | 5 | import argparse 6 | import os 7 | import yaml 8 | import sys 9 | import numpy as np 10 | 11 | # possible splits 12 | splits = ["train", "valid", "test"] 13 | 14 | # possible backends 15 | backends = ["numpy", "torch"] 16 | 17 | if __name__ == '__main__': 18 | parser = argparse.ArgumentParser("./evaluate_mos.py") 19 | parser.add_argument( 20 | '--dataset', '-d', 21 | type=str, 22 | required=True, 23 | help='Dataset dir. No Default', 24 | ) 25 | parser.add_argument( 26 | '--predictions', '-p', 27 | type=str, 28 | required=None, 29 | help='Prediction dir. Same organization as dataset, but predictions in' 30 | 'each sequences "prediction" directory. No Default. If no option is set' 31 | ' we look for the labels in the same directory as dataset' 32 | ) 33 | parser.add_argument( 34 | '--split', '-s', 35 | type=str, 36 | required=False, 37 | choices=["train", "valid", "test"], 38 | default="valid", 39 | help='Split to evaluate on. One of ' + 40 | str(splits) + '. Defaults to %(default)s', 41 | ) 42 | parser.add_argument( 43 | '--backend', '-b', 44 | type=str, 45 | required=False, 46 | choices= ["numpy", "torch"], 47 | default="numpy", 48 | help='Backend for evaluation. One of ' + 49 | str(backends) + ' Defaults to %(default)s', 50 | ) 51 | parser.add_argument( 52 | '--datacfg', '-dc', 53 | type=str, 54 | required=False, 55 | default="config/semantic-kitti-mos.yaml", 56 | help='Dataset config file. Defaults to %(default)s', 57 | ) 58 | parser.add_argument( 59 | '--limit', '-l', 60 | type=int, 61 | required=False, 62 | default=None, 63 | help='Limit to the first "--limit" points of each scan. Useful for' 64 | ' evaluating single scan from aggregated pointcloud.' 65 | ' Defaults to %(default)s', 66 | ) 67 | parser.add_argument( 68 | '--codalab', 69 | dest='codalab', 70 | type=str, 71 | default=None, 72 | help='Exports "scores.txt" to given output directory for codalab' 73 | 'Defaults to %(default)s', 74 | ) 75 | 76 | FLAGS, unparsed = parser.parse_known_args() 77 | 78 | # fill in real predictions dir 79 | if FLAGS.predictions is None: 80 | FLAGS.predictions = FLAGS.dataset 81 | 82 | # print summary of what we will do 83 | print("*" * 80) 84 | print("INTERFACE:") 85 | print("Data: ", FLAGS.dataset) 86 | print("Predictions: ", FLAGS.predictions) 87 | print("Backend: ", FLAGS.backend) 88 | print("Split: ", FLAGS.split) 89 | print("Config: ", FLAGS.datacfg) 90 | print("Limit: ", FLAGS.limit) 91 | print("Codalab: ", FLAGS.codalab) 92 | print("*" * 80) 93 | 94 | # assert split 95 | assert(FLAGS.split in splits) 96 | 97 | # assert backend 98 | assert(FLAGS.backend in backends) 99 | 100 | print("Opening data config file %s" % FLAGS.datacfg) 101 | DATA = yaml.safe_load(open(FLAGS.datacfg, 'r')) 102 | 103 | # get number of interest classes, and the label mappings 104 | class_strings = DATA["labels"] 105 | class_remap = DATA["learning_map"] 106 | class_inv_remap = DATA["learning_map_inv"] 107 | class_ignore = DATA["learning_ignore"] 108 | nr_classes = len(class_inv_remap) 109 | 110 | # make lookup table for mapping 111 | maxkey = max(class_remap.keys()) 112 | 113 | # +100 hack making lut bigger just in case there are unknown labels 114 | remap_lut = np.zeros((maxkey + 100), dtype=np.int32) 115 | remap_lut[list(class_remap.keys())] = list(class_remap.values()) 116 | # print(remap_lut) 117 | 118 | # create evaluator 119 | ignore = [] 120 | for cl, ign in class_ignore.items(): 121 | if ign: 122 | x_cl = int(cl) 123 | ignore.append(x_cl) 124 | print("Ignoring xentropy class ", x_cl, " in IoU evaluation") 125 | 126 | # create evaluator 127 | if FLAGS.backend == "torch": 128 | from auxiliary.torch_ioueval import iouEval 129 | evaluator = iouEval(nr_classes, ignore) 130 | if FLAGS.backend == "numpy": 131 | from auxiliary.np_ioueval import iouEval 132 | evaluator = iouEval(nr_classes, ignore) 133 | else: 134 | print("Backend for evaluator should be one of ", str(backends)) 135 | quit() 136 | evaluator.reset() 137 | 138 | # get test set 139 | test_sequences = DATA["split"][FLAGS.split] 140 | 141 | # get label paths 142 | label_names = [] 143 | for sequence in test_sequences: 144 | sequence = '{0:02d}'.format(int(sequence)) 145 | label_paths = os.path.join(FLAGS.dataset, "sequences", 146 | str(sequence), "labels") 147 | # populate the label names 148 | seq_label_names = [os.path.join(dp, f) for dp, dn, fn in os.walk( 149 | os.path.expanduser(label_paths)) for f in fn if ".label" in f] 150 | seq_label_names.sort() 151 | label_names.extend(seq_label_names) 152 | # print(label_names) 153 | 154 | # get predictions paths 155 | pred_names = [] 156 | for sequence in test_sequences: 157 | sequence = '{0:02d}'.format(int(sequence)) 158 | pred_paths = os.path.join(FLAGS.predictions, "sequences", 159 | sequence, "predictions") 160 | # populate the label names 161 | seq_pred_names = [os.path.join(dp, f) for dp, dn, fn in os.walk( 162 | os.path.expanduser(pred_paths)) for f in fn if ".label" in f] 163 | seq_pred_names.sort() 164 | pred_names.extend(seq_pred_names) 165 | # print(pred_names) 166 | 167 | # check that I have the same number of files 168 | print("labels: ", len(label_names)) 169 | print("predictions: ", len(pred_names)) 170 | # assert(len(label_names) == len(pred_names)) 171 | 172 | progress = 10 173 | count = 0 174 | print("Evaluating sequences: ", end="", flush=True) 175 | # open each file, get the tensor, and make the iou comparison 176 | for label_file, pred_file in zip(label_names[:], pred_names[:]): 177 | count += 1 178 | if 100 * count / len(label_names) > progress: 179 | print("{:d}% ".format(progress), end="", flush=True) 180 | progress += 10 181 | 182 | # print("evaluating label ", label_file) 183 | # open label 184 | label = np.fromfile(label_file, dtype=np.int32) 185 | label = label.reshape((-1)) # reshape to vector 186 | label = label & 0xFFFF # get lower half for semantics 187 | if FLAGS.limit is not None: 188 | label = label[:FLAGS.limit] # limit to desired length 189 | label = remap_lut[label] # remap to xentropy format 190 | 191 | # open prediction 192 | pred = np.fromfile(pred_file, dtype=np.int32) 193 | pred = pred.reshape((-1)) # reshape to vector 194 | pred = pred & 0xFFFF # get lower half for semantics 195 | if FLAGS.limit is not None: 196 | pred = pred[:FLAGS.limit] # limit to desired length 197 | pred = remap_lut[pred] # remap to xentropy format 198 | 199 | # add single scan to evaluation 200 | evaluator.addBatch(pred, label) 201 | 202 | # when I am done, print the evaluation 203 | m_accuracy = evaluator.getacc() 204 | m_jaccard, class_jaccard = evaluator.getIoU() 205 | 206 | # print for spreadsheet 207 | print("*" * 80) 208 | print("below can be copied straight for paper table") 209 | for i, jacc in enumerate(class_jaccard): 210 | if i not in ignore: 211 | if int(class_inv_remap[i]) > 250: 212 | sys.stdout.write('iou_moving: {jacc:.3f}'.format(jacc=jacc.item())) 213 | sys.stdout.write('\n') 214 | sys.stdout.flush() 215 | 216 | # if codalab is necessary, then do it 217 | # for moving object detection, we only care about the results of moving objects 218 | if FLAGS.codalab is not None: 219 | results = {} 220 | for i, jacc in enumerate(class_jaccard): 221 | if i not in ignore: 222 | if int(class_inv_remap[i]) > 250: 223 | results["iou_moving"] = float(jacc) 224 | # save to file 225 | output_filename = os.path.join(FLAGS.codalab, 'scores.txt') 226 | with open(output_filename, 'w') as yaml_file: 227 | yaml.dump(results, yaml_file, default_flow_style=False) 228 | -------------------------------------------------------------------------------- /evaluate_completion.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import argparse 4 | import numpy as np 5 | import scipy.io as sio 6 | import yaml 7 | import os 8 | import time 9 | 10 | epsilon = np.finfo(np.float32).eps 11 | 12 | 13 | def get_eval_mask(labels, invalid_voxels): 14 | """ 15 | Ignore labels set to 255 and invalid voxels (the ones never hit by a laser ray, probed using ray tracing) 16 | :param labels: input ground truth voxels 17 | :param invalid_voxels: voxels ignored during evaluation since the lie beyond the scene that was captured by the laser 18 | :return: boolean mask to subsample the voxels to evaluate 19 | """ 20 | masks = np.ones_like(labels, dtype=np.bool) 21 | masks[labels == 255] = False 22 | masks[invalid_voxels == 1] = False 23 | 24 | return masks 25 | 26 | 27 | def unpack(compressed): 28 | ''' given a bit encoded voxel grid, make a normal voxel grid out of it. ''' 29 | uncompressed = np.zeros(compressed.shape[0] * 8, dtype=np.uint8) 30 | uncompressed[::8] = compressed[:] >> 7 & 1 31 | uncompressed[1::8] = compressed[:] >> 6 & 1 32 | uncompressed[2::8] = compressed[:] >> 5 & 1 33 | uncompressed[3::8] = compressed[:] >> 4 & 1 34 | uncompressed[4::8] = compressed[:] >> 3 & 1 35 | uncompressed[5::8] = compressed[:] >> 2 & 1 36 | uncompressed[6::8] = compressed[:] >> 1 & 1 37 | uncompressed[7::8] = compressed[:] & 1 38 | 39 | return uncompressed 40 | 41 | 42 | def load_gt_volume(filename): 43 | basename = os.path.splitext(filename)[0] 44 | 45 | labels = np.fromfile(filename, dtype=np.uint16) 46 | invalid_voxels = unpack(np.fromfile(basename + ".invalid", dtype=np.uint8)) 47 | 48 | return labels, invalid_voxels 49 | 50 | def load_pred_volume(filename): 51 | labels = np.fromfile(filename, dtype=np.uint16) 52 | return labels 53 | 54 | # possible splits 55 | splits = ["train", "valid", "test"] 56 | 57 | if __name__ == "__main__": 58 | parser = argparse.ArgumentParser(description="SSC semantic-kitti") 59 | 60 | parser.add_argument( 61 | '--dataset', '-d', 62 | type=str, 63 | required=True, 64 | help='Dataset dir. No Default', 65 | ) 66 | 67 | parser.add_argument( 68 | '--predictions', '-p', 69 | type=str, 70 | required=False, 71 | help='Prediction dir. Same organization as dataset, but predictions in' 72 | 'each sequences "prediction" directory.' 73 | ) 74 | parser.add_argument( 75 | '--datacfg', '-dc', 76 | type=str, 77 | required=False, 78 | default="config/semantic-kitti.yaml", 79 | help='Dataset config file. Defaults to %(default)s', 80 | ) 81 | 82 | parser.add_argument( 83 | '--split', '-s', 84 | type=str, 85 | required=False, 86 | choices=["train", "valid", "test"], 87 | default="valid", 88 | help='Split to evaluate on. One of ' + 89 | str(splits) + '. Defaults to %(default)s', 90 | ) 91 | parser.add_argument( 92 | '--output', 93 | dest='output', 94 | type=str, 95 | default=".", 96 | help='Exports "scores.txt" to given output directory for codalab' 97 | 'Defaults to %(default)s', 98 | ) 99 | 100 | args = parser.parse_args() 101 | print(" ========================== Arguments ========================== ") 102 | print("\n".join([" {}:\t{}".format(k,v) for (k,v) in vars(args).items()])) 103 | print(" =============================================================== \n") 104 | gt_data_root = args.dataset 105 | 106 | DATA = yaml.safe_load(open(args.datacfg, 'r')) 107 | 108 | # get number of interest classes, and the label mappings 109 | class_strings = DATA["labels"] 110 | class_remap = DATA["learning_map"] 111 | class_inv_remap = DATA["learning_map_inv"] 112 | class_ignore = DATA["learning_ignore"] 113 | n_classes = len(class_inv_remap) 114 | 115 | test_sequences = DATA["split"][args.split] 116 | 117 | # make lookup table for mapping 118 | maxkey = max(class_remap.keys()) 119 | 120 | # +100 hack making lut bigger just in case there are unknown labels 121 | remap_lut = np.zeros((maxkey + 100), dtype=np.int32) 122 | remap_lut[list(class_remap.keys())] = list(class_remap.values()) 123 | 124 | # in completion we have to distinguish empty and invalid voxels. 125 | # Important: For voxels 0 corresponds to "empty" and not "unlabeled". 126 | remap_lut[remap_lut == 0] = 255 # map 0 to 'invalid' 127 | remap_lut[0] = 0 # only 'empty' stays 'empty'. 128 | 129 | from auxiliary.np_ioueval import iouEval 130 | evaluator = iouEval(n_classes, []) 131 | 132 | # get files from ground truth and predictions. 133 | filenames_gt = [] 134 | filenames_pred = [] 135 | for seq in test_sequences: 136 | seq_dir_gt = os.path.join("sequences", '{0:02d}'.format(int(seq)), "voxels") 137 | seq_dir_pred = os.path.join("sequences", '{0:02d}'.format(int(seq)), "predictions") 138 | 139 | gt_file_list = [f for f in os.listdir(os.path.join(args.dataset, seq_dir_gt)) if f.endswith(".label")] 140 | filenames_gt.extend([os.path.join(seq_dir_gt, f) for f in gt_file_list]) 141 | filenames_pred.extend([os.path.join(seq_dir_pred, f) for f in gt_file_list]) 142 | 143 | missing_pred_files = False 144 | 145 | if args.predictions is None: 146 | prediction_dir = args.dataset 147 | else: 148 | prediction_dir = args.predictions 149 | 150 | # check that all prediction files exist 151 | for pred_file in filenames_pred: 152 | if not os.path.exists(os.path.join(prediction_dir, pred_file)): 153 | print("Expected to have {}, but file does not exist!".format(pred_file)) 154 | missing_pred_files = True 155 | 156 | if missing_pred_files: raise RuntimeError("Error: Missing prediction files! Aborting evaluation.") 157 | 158 | evaluation_pairs = list(zip(filenames_gt, filenames_pred)) 159 | 160 | print("Evaluating: ", end="", flush=True) 161 | progress = 10 162 | 163 | for i, f in enumerate(evaluation_pairs): 164 | if 100.0 * i / len(evaluation_pairs) >= progress: 165 | print("{}% ".format(progress), end="", flush=True) 166 | progress = progress + 10 167 | 168 | filename_gt = os.path.join(args.dataset, f[0]) 169 | filename_pred = os.path.join(prediction_dir, f[1]) 170 | 171 | pred = load_pred_volume(filename_pred) 172 | target, invalid_voxels = load_gt_volume(filename_gt) 173 | 174 | # Map labels "pred_labels" and "gt_labels" from semantic-kitti ID's to [0 : n_classes -1] 175 | pred = remap_lut[pred] 176 | target = remap_lut[target] 177 | 178 | masks = get_eval_mask(target, invalid_voxels) 179 | 180 | target = target[masks] 181 | pred = pred[masks] 182 | 183 | # add single scan to evaluation 184 | evaluator.addBatch(pred, target) 185 | print("Done \U0001F389.") 186 | print("\n ========================== RESULTS ========================== ") 187 | # when I am done, print the evaluation 188 | _, class_jaccard = evaluator.getIoU() 189 | m_jaccard = class_jaccard[1:].mean() 190 | 191 | 192 | print('Validation set:\nIoU avg {m_jaccard:.3f}'.format(m_jaccard=m_jaccard)) 193 | ignore = [0] 194 | # print also classwise 195 | for i, jacc in enumerate(class_jaccard): 196 | if i not in ignore: 197 | print('IoU class {i:} [{class_str:}] = {jacc:.3f}'.format( 198 | i=i, class_str=class_strings[class_inv_remap[i]], jacc=jacc)) 199 | 200 | # compute remaining metrics. 201 | conf = evaluator.get_confusion() 202 | precision = np.sum(conf[1:,1:]) / (np.sum(conf[1:,:]) + epsilon) 203 | recall = np.sum(conf[1:,1:]) / (np.sum(conf[:,1:]) + epsilon) 204 | acc_cmpltn = (np.sum(conf[1:, 1:])) / (np.sum(conf) - conf[0,0]) 205 | mIoU_ssc = m_jaccard 206 | 207 | print("Precision =\t" + str(np.round(precision * 100, 2)) + '\n' + 208 | "Recall =\t" + str(np.round(recall * 100, 2)) + '\n' + 209 | "IoU Cmpltn =\t" + str(np.round(acc_cmpltn * 100, 2)) + '\n' + 210 | "mIoU SSC =\t" + str(np.round(mIoU_ssc * 100, 2))) 211 | 212 | # write "scores.txt" with all information 213 | results = {} 214 | results["iou_completion"] = float(acc_cmpltn) 215 | results["iou_mean"] = float(mIoU_ssc) 216 | 217 | for i, jacc in enumerate(class_jaccard): 218 | if i not in ignore: 219 | results["iou_"+class_strings[class_inv_remap[i]]] = float(jacc) 220 | 221 | output_filename = os.path.join(args.output, 'scores.txt') 222 | with open(output_filename, 'w') as yaml_file: 223 | yaml.dump(results, yaml_file, default_flow_style=False) -------------------------------------------------------------------------------- /validate_submission.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # This file is covered by the LICENSE file in the root of this project. 3 | 4 | import zipfile 5 | import argparse 6 | import os 7 | import numpy as np 8 | 9 | 10 | class ValidationException(Exception): 11 | pass 12 | 13 | def unpack(compressed): 14 | ''' given a bit encoded voxel grid, make a normal voxel grid out of it. ''' 15 | uncompressed = np.zeros(compressed.shape[0] * 8, dtype=np.uint8) 16 | uncompressed[::8] = compressed[:] >> 7 & 1 17 | uncompressed[1::8] = compressed[:] >> 6 & 1 18 | uncompressed[2::8] = compressed[:] >> 5 & 1 19 | uncompressed[3::8] = compressed[:] >> 4 & 1 20 | uncompressed[4::8] = compressed[:] >> 3 & 1 21 | uncompressed[5::8] = compressed[:] >> 2 & 1 22 | uncompressed[6::8] = compressed[:] >> 1 & 1 23 | uncompressed[7::8] = compressed[:] & 1 24 | 25 | return uncompressed 26 | 27 | if __name__ == "__main__": 28 | parser = argparse.ArgumentParser( 29 | description="Validate a submission zip file needed to evaluate on CodaLab competitions.\n\nThe verification tool checks:\n 1. correct folder structure,\n 2. existence of label files for each scan,\n 3. count of labels for each scan.\nInvalid labels are ignored by the evaluation script, therefore we don't check\nfor invalid labels.", formatter_class=argparse.RawTextHelpFormatter) 30 | 31 | parser.add_argument( 32 | "zipfile", 33 | type=str, 34 | help='zip file that should be validated.', 35 | ) 36 | 37 | parser.add_argument( 38 | 'dataset', 39 | type=str, 40 | help='directory containing the folder "sequences" containing folders "11", ..., "21" with the input data ("velodyne" or "voxels") folder.' 41 | ) 42 | 43 | parser.add_argument( 44 | "--task", 45 | type=str, 46 | choices=["segmentation", "completion", "panoptic"], 47 | default="segmentation", 48 | help='task for which the zip file should be validated.' 49 | ) 50 | 51 | FLAGS, _ = parser.parse_known_args() 52 | 53 | checkmark = "\u2713" 54 | 55 | float_bytes = 4 56 | uint32_bytes = 4 57 | uint16_bytes = 2 58 | 59 | try: 60 | 61 | print('Validating zip archive "{}".\n'.format(FLAGS.zipfile)) 62 | 63 | print( " ============ {:^10} ============ ".format(FLAGS.task)) 64 | 65 | print(" 1. Checking filename.............. ", end="", flush=True) 66 | if not FLAGS.zipfile.endswith('.zip'): 67 | raise ValidationException('Competition bundle must end with ".zip"') 68 | print(checkmark) 69 | 70 | with zipfile.ZipFile(FLAGS.zipfile) as zipfile: 71 | if FLAGS.task == "segmentation" or FLAGS.task == "panoptic": 72 | 73 | 74 | print(" 2. Checking directory structure... ", end="", flush=True) 75 | 76 | directories = [folder.filename for folder in zipfile.infolist() if folder.filename.endswith("/")] 77 | if "sequences/" not in directories: 78 | raise ValidationException('Directory "sequences" missing inside zip file.') 79 | 80 | for sequence in range(11, 22): 81 | sequence_directory = "sequences/{}/".format(sequence) 82 | if sequence_directory not in directories: 83 | raise ValidationException('Directory "{}" missing inside zip file.'.format(sequence_directory)) 84 | predictions_directory = sequence_directory + "predictions/" 85 | if predictions_directory not in directories: 86 | raise ValidationException('Directory "{}" missing inside zip file.'.format(predictions_directory)) 87 | 88 | print(checkmark) 89 | 90 | print(' 3. Checking file sizes............ ', end='', flush=True) 91 | 92 | prediction_files = {info.filename: info for info in zipfile.infolist() if not info.filename.endswith("/")} 93 | 94 | for sequence in range(11, 22): 95 | sequence_directory = 'sequences/{}'.format(sequence) 96 | velodyne_directory = os.path.join(FLAGS.dataset, 'sequences/{}/velodyne/'.format(sequence)) 97 | 98 | velodyne_files = sorted([os.path.join(velodyne_directory, file) for file in os.listdir(velodyne_directory)]) 99 | label_files = sorted([os.path.join(sequence_directory, "predictions", os.path.splitext(filename)[0] + ".label") 100 | for filename in os.listdir(velodyne_directory)]) 101 | 102 | for velodyne_file, label_file in zip(velodyne_files, label_files): 103 | num_points = os.path.getsize(velodyne_file) / (4 * float_bytes) 104 | 105 | if label_file not in prediction_files: 106 | raise ValidationException('"' + label_file + '" is missing inside zip.') 107 | 108 | num_labels = prediction_files[label_file].file_size / uint32_bytes 109 | if num_labels != num_points: 110 | raise ValidationException('label file "' + label_file + 111 | "' should have {} labels, but found {} labels!".format(int(num_points), int(num_labels))) 112 | 113 | print(checkmark) 114 | elif FLAGS.task == "completion": 115 | print(" 2. Checking directory structure... ", end="", flush=True) 116 | 117 | directories = [folder.filename for folder in zipfile.infolist() if folder.filename.endswith("/")] 118 | if "sequences/" not in directories: 119 | raise ValidationException('Directory "sequences" missing inside zip file.') 120 | 121 | for sequence in range(11, 22): 122 | sequence_directory = "sequences/{}/".format(sequence) 123 | if sequence_directory not in directories: 124 | raise ValidationException('Directory "{}" missing inside zip file.'.format(sequence_directory)) 125 | predictions_directory = sequence_directory + "predictions/" 126 | if predictions_directory not in directories: 127 | raise ValidationException('Directory "{}" missing inside zip file.'.format(predictions_directory)) 128 | 129 | print(checkmark) 130 | 131 | print(' 3. Checking file sizes', end='', flush=True) 132 | 133 | prediction_files = {str(info.filename): info for info in zipfile.infolist() if not info.filename.endswith("/")} 134 | 135 | # description.txt is optional and one should not get an error. 136 | if "description.txt" in prediction_files: del prediction_files["description.txt"] 137 | 138 | 139 | necessary_files = [] 140 | 141 | for sequence in range(11, 22): 142 | 143 | sequence_directory = 'sequences/{}'.format(sequence) 144 | voxel_directory = os.path.join(FLAGS.dataset, 'sequences/{}/voxels/'.format(sequence)) 145 | 146 | voxel_files = sorted([os.path.join(voxel_directory, file) for file in os.listdir(voxel_directory) if file.endswith(".bin")]) 147 | label_files = sorted([os.path.join(sequence_directory, "predictions", os.path.splitext(filename)[0] + ".label") 148 | for filename in os.listdir(voxel_directory)]) 149 | necessary_files.extend(label_files) 150 | 151 | for voxel_file, label_file in zip(voxel_files, label_files): 152 | input_voxels = unpack(np.fromfile(voxel_file, dtype=np.uint8)) 153 | num_voxels = input_voxels.shape[0] # fixed volume (= 256 * 256 * 32)! 154 | 155 | if label_file not in prediction_files: 156 | raise ValidationException('"' + label_file + '" is missing inside zip.') 157 | 158 | num_labels = prediction_files[label_file].file_size / uint16_bytes # expecting uint16 for labels. 159 | if num_labels != num_voxels: 160 | raise ValidationException('label file "' + label_file + 161 | "' should have {} labels, but found {} labels!".format(int(num_voxels), int(num_labels))) 162 | print(".", end="", flush=True) 163 | print(". ", end="", flush=True) 164 | print(checkmark) 165 | 166 | print(' 4. Checking for unneeded files', end='', flush=True) 167 | if len(necessary_files) != len(prediction_files.keys()): 168 | filelist = sorted([f for f in prediction_files.keys() if f not in necessary_files]) 169 | ell = "" 170 | if len(filelist) > 10: ell = ", ..." 171 | raise ValidationException("Zip contains unneeded predictions, e.g., {}".format(",".join(filelist[:10]) + ell)) 172 | 173 | print(".... " + checkmark) 174 | else: 175 | raise NotImplementedError("Unknown task.") 176 | except ValidationException as ex: 177 | print("\n\n " + "\u001b[1;31m>>> Error: " + str(ex) + "\u001b[0m") 178 | exit(1) 179 | 180 | print("\n\u001b[1;32mEverything ready for submission!\u001b[0m \U0001F389") 181 | -------------------------------------------------------------------------------- /auxiliary/glow.py: -------------------------------------------------------------------------------- 1 | import OpenGL.GL as gl 2 | gl.ERROR_CHECKING = True 3 | gl.ERROR_ON_COPY = True 4 | gl.WARN_ON_FORMAT_UNAVAILABLE = True 5 | import numpy as np 6 | import re 7 | """ 8 | openGL Object Wrapper (GLOW) in python. 9 | 10 | Some convenience classes to simplify resource management 11 | 12 | """ 13 | 14 | WARN_INVALID_UNIFORMS = False 15 | 16 | 17 | def vec2(x, y): 18 | """ returns an vec2-compatible numpy array """ 19 | return np.array([x, y], dtype=np.float32) 20 | 21 | 22 | def vec3(x, y, z): 23 | """ returns an vec3-compatible numpy array """ 24 | return np.array([x, y, z], dtype=np.float32) 25 | 26 | 27 | def vec4(x, y, z, w): 28 | """ returns an vec4-compatible numpy array """ 29 | return np.array([x, y, z, w], dtype=np.float32) 30 | 31 | 32 | def ivec2(x, y): 33 | """ returns an ivec2-compatible numpy array """ 34 | return np.array([x, y], dtype=np.int32) 35 | 36 | 37 | def ivec3(x, y, z): 38 | """ returns an ivec3-compatible numpy array """ 39 | return np.array([x, y, z], dtype=np.int32) 40 | 41 | 42 | def ivec4(x, y, z, w): 43 | """ returns an ivec4-compatible numpy array """ 44 | return np.array([x, y, z, w], dtype=np.int32) 45 | 46 | 47 | def uivec2(x, y): 48 | """ returns an ivec2-compatible numpy array """ 49 | return np.array([x, y], dtype=np.uint32) 50 | 51 | 52 | def uivec3(x, y, z): 53 | """ returns an ivec3-compatible numpy array """ 54 | return np.array([x, y, z], dtype=np.uint32) 55 | 56 | 57 | def uivec4(x, y, z, w): 58 | """ returns an ivec4-compatible numpy array """ 59 | return np.array([x, y, z, w], dtype=np.uint32) 60 | 61 | 62 | class GlBuffer: 63 | """ 64 | Buffer object representing a vertex array buffer. 65 | """ 66 | 67 | def __init__(self, target=gl.GL_ARRAY_BUFFER, usage=gl.GL_STATIC_DRAW): 68 | self.id_ = gl.glGenBuffers(1) 69 | self.target_ = target 70 | self.usage_ = usage 71 | 72 | # def __del__(self): 73 | # gl.glDeleteBuffers(1, self.id_) 74 | 75 | def assign(self, array): 76 | gl.glBindBuffer(self.target_, self.id_) 77 | gl.glBufferData(self.target_, array, self.usage_) 78 | gl.glBindBuffer(self.target_, 0) 79 | 80 | def bind(self): 81 | gl.glBindBuffer(self.target_, self.id_) 82 | 83 | def release(self): 84 | gl.glBindBuffer(self.target_, 0) 85 | 86 | @property 87 | def id(self): 88 | return self.id_ 89 | 90 | @property 91 | def usage(self): 92 | return self.usage_ 93 | 94 | @property 95 | def target(self): 96 | return self.target_ 97 | 98 | 99 | class GlTextureRectangle: 100 | def __init__(self, width, height, internalFormat=gl.GL_RGBA, format=gl.GL_RGBA): 101 | self.id_ = gl.glGenTextures(1) 102 | self.internalFormat_ = internalFormat # gl.GL_RGB_FLOAT, gl.GL_RGB_UNSIGNED, ... 103 | self.format = format # GL_RG. GL_RG_INTEGER, ... 104 | 105 | self.width_ = width 106 | self.height_ = height 107 | 108 | gl.glBindTexture(gl.GL_TEXTURE_RECTANGLE, self.id_) 109 | gl.glTexParameteri(gl.GL_TEXTURE_RECTANGLE, gl.GL_TEXTURE_MIN_FILTER, gl.GL_NEAREST) 110 | gl.glTexParameteri(gl.GL_TEXTURE_RECTANGLE, gl.GL_TEXTURE_MAG_FILTER, gl.GL_NEAREST) 111 | gl.glTexParameteri(gl.GL_TEXTURE_RECTANGLE, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_BORDER) 112 | gl.glTexParameteri(gl.GL_TEXTURE_RECTANGLE, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_BORDER) 113 | gl.glBindTexture(gl.GL_TEXTURE_RECTANGLE, 0) 114 | 115 | def bind(self, textureUnitId): 116 | gl.glActiveTexture(gl.GL_TEXTURE0 + int(textureUnitId)) 117 | gl.glBindTexture(gl.GL_TEXTURE_RECTANGLE, self.id_) 118 | 119 | def release(self, textureUnitId): 120 | gl.glActiveTexture(gl.GL_TEXTURE0 + int(textureUnitId)) 121 | gl.glBindTexture(gl.GL_TEXTURE_RECTANGLE, 0) 122 | 123 | def assign(self, array): 124 | gl.glBindTexture(gl.GL_TEXTURE_RECTANGLE, self.id_) 125 | 126 | if array.dtype == np.uint8: 127 | gl.glTexImage2D(gl.GL_TEXTURE_RECTANGLE, 0, self.internalFormat_, self.width_, self.height_, 0, self.format, 128 | gl.GL_UNSIGNED_BYTE, array) 129 | elif array.dtype == np.float32: 130 | gl.glTexImage2D(gl.GL_TEXTURE_RECTANGLE, 0, self.internalFormat_, self.width_, self.height_, 0, self.format, 131 | gl.GL_FLOAT, array) 132 | else: 133 | raise NotImplementedError("pixel type not implemented.") 134 | 135 | gl.glBindTexture(gl.GL_TEXTURE_RECTANGLE, 0) 136 | 137 | @property 138 | def id(self): 139 | return self.id_ 140 | 141 | 142 | class GlShader: 143 | def __init__(self, shader_type, source): 144 | self.code_ = source 145 | self.shader_type_ = shader_type 146 | 147 | self.id_ = gl.glCreateShader(self.shader_type_) 148 | gl.glShaderSource(self.id_, source) 149 | 150 | gl.glCompileShader(self.id_) 151 | 152 | success = gl.glGetShaderiv(self.id_, gl.GL_COMPILE_STATUS) 153 | if success == gl.GL_FALSE: 154 | error_string = gl.glGetShaderInfoLog(self.id_).decode("utf-8") 155 | raise RuntimeError(error_string) 156 | 157 | def __del__(self): 158 | gl.glDeleteShader(self.id_) 159 | 160 | @property 161 | def type(self): 162 | return self.shader_type_ 163 | 164 | @property 165 | def id(self): 166 | return self.id_ 167 | 168 | @property 169 | def code(self): 170 | return self.code_ 171 | 172 | @staticmethod 173 | def fromFile(shader_type, filename): 174 | f = open(filename) 175 | source = "\n".join(f.readlines()) 176 | # todo: preprocess. 177 | f.close() 178 | 179 | return GlShader(shader_type, source) 180 | 181 | 182 | class GlProgram: 183 | """ An OpenGL program handle. """ 184 | 185 | def __init__(self): 186 | self.id_ = gl.glCreateProgram() 187 | self.shaders_ = {} 188 | self.uniform_types_ = {} 189 | self.is_linked = False 190 | 191 | def __del__(self): 192 | gl.glDeleteProgram(self.id_) 193 | 194 | def bind(self): 195 | if not self.is_linked: 196 | raise RuntimeError("Program must be linked before usage.") 197 | gl.glUseProgram(self.id_) 198 | 199 | def release(self): 200 | gl.glUseProgram(0) 201 | 202 | def attach(self, shader): 203 | self.shaders_[shader.type] = shader 204 | 205 | def __setitem__(self, name, value): 206 | # quitely ignore 207 | if name not in self.uniform_types_: 208 | if WARN_INVALID_UNIFORMS: print("No uniform with name '{}' available.".format(name)) 209 | return 210 | 211 | loc = gl.glGetUniformLocation(self.id_, name) 212 | T = self.uniform_types_[name] 213 | 214 | if T == "int": 215 | gl.glUniform1i(loc, np.int32(value)) 216 | if T == "uint": 217 | gl.glUniform1ui(loc, np.uint32(value)) 218 | elif T == "float": 219 | gl.glUniform1f(loc, np.float32(value)) 220 | elif T == "bool": 221 | gl.glUniform1f(loc, value) 222 | elif T == "vec2": 223 | gl.glUniform2fv(loc, 1, value) 224 | elif T == "vec3": 225 | gl.glUniform3fv(loc, 1, value) 226 | elif T == "vec4": 227 | gl.glUniform4fv(loc, 1, value) 228 | elif T == "ivec2": 229 | gl.glUniform2iv(loc, 1, value) 230 | elif T == "ivec3": 231 | gl.glUniform3iv(loc, 1, value) 232 | elif T == "ivec4": 233 | gl.glUniform4iv(loc, 1, value) 234 | elif T == "uivec2": 235 | gl.glUniform2uiv(loc, 1, value) 236 | elif T == "uivec3": 237 | gl.glUniform3uiv(loc, 1, value) 238 | elif T == "uivec4": 239 | gl.glUniform4uiv(loc, 1, value) 240 | elif T == "mat4": 241 | #print("set matrix: ", value) 242 | gl.glUniformMatrix4fv(loc, 1, False, value.astype(np.float32)) 243 | elif T == "sampler2D": 244 | gl.glUniform1i(loc, np.int32(value)) 245 | elif T == "sampler2DRect": 246 | gl.glUniform1i(loc, np.int32(value)) 247 | else: 248 | raise NotImplementedError("uniform type {} not implemented. :(".format(T)) 249 | 250 | def link(self): 251 | if gl.GL_VERTEX_SHADER not in self.shaders_ or gl.GL_FRAGMENT_SHADER not in self.shaders_: 252 | raise RuntimeError("program needs at least vertex and fragment shader") 253 | 254 | for shader in self.shaders_.values(): 255 | gl.glAttachShader(self.id_, shader.id) 256 | for line in shader.code.split("\n"): 257 | match = re.search(r"uniform\s+(\S+)\s+(\S+)\s*;", line) 258 | if match: 259 | self.uniform_types_[match.group(2)] = match.group(1) 260 | 261 | gl.glLinkProgram(self.id_) 262 | isLinked = bool(gl.glGetProgramiv(self.id_, gl.GL_LINK_STATUS)) 263 | if not isLinked: 264 | msg = gl.glGetProgramInfoLog(self.id_) 265 | 266 | raise RuntimeError(str(msg.decode("utf-8"))) 267 | 268 | # after linking we don't need the source code anymore. 269 | self.shaders_ = {} 270 | self.is_linked = True 271 | -------------------------------------------------------------------------------- /evaluate_semantics.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # This file is covered by the LICENSE file in the root of this project. 3 | 4 | import argparse 5 | import os 6 | import yaml 7 | import sys 8 | import numpy as np 9 | 10 | # possible splits 11 | splits = ["train", "valid", "test"] 12 | 13 | # possible backends 14 | backends = ["numpy", "torch"] 15 | 16 | if __name__ == '__main__': 17 | parser = argparse.ArgumentParser("./evaluate_semantics.py") 18 | parser.add_argument( 19 | '--dataset', '-d', 20 | type=str, 21 | required=True, 22 | help='Dataset dir. No Default', 23 | ) 24 | parser.add_argument( 25 | '--predictions', '-p', 26 | type=str, 27 | required=None, 28 | help='Prediction dir. Same organization as dataset, but predictions in' 29 | 'each sequences "prediction" directory. No Default. If no option is set' 30 | ' we look for the labels in the same directory as dataset' 31 | ) 32 | parser.add_argument( 33 | '--split', '-s', 34 | type=str, 35 | required=False, 36 | choices=["train", "valid", "test"], 37 | default="valid", 38 | help='Split to evaluate on. One of ' + 39 | str(splits) + '. Defaults to %(default)s', 40 | ) 41 | parser.add_argument( 42 | '--backend', '-b', 43 | type=str, 44 | required=False, 45 | choices= ["numpy", "torch"], 46 | default="numpy", 47 | help='Backend for evaluation. One of ' + 48 | str(backends) + ' Defaults to %(default)s', 49 | ) 50 | parser.add_argument( 51 | '--datacfg', '-dc', 52 | type=str, 53 | required=False, 54 | default="config/semantic-kitti.yaml", 55 | help='Dataset config file. Defaults to %(default)s', 56 | ) 57 | parser.add_argument( 58 | '--limit', '-l', 59 | type=int, 60 | required=False, 61 | default=None, 62 | help='Limit to the first "--limit" points of each scan. Useful for' 63 | ' evaluating single scan from aggregated pointcloud.' 64 | ' Defaults to %(default)s', 65 | ) 66 | parser.add_argument( 67 | '--codalab', 68 | dest='codalab', 69 | type=str, 70 | default=None, 71 | help='Exports "scores.txt" to given output directory for codalab' 72 | 'Defaults to %(default)s', 73 | ) 74 | 75 | FLAGS, unparsed = parser.parse_known_args() 76 | 77 | # fill in real predictions dir 78 | if FLAGS.predictions is None: 79 | FLAGS.predictions = FLAGS.dataset 80 | 81 | # print summary of what we will do 82 | print("*" * 80) 83 | print("INTERFACE:") 84 | print("Data: ", FLAGS.dataset) 85 | print("Predictions: ", FLAGS.predictions) 86 | print("Backend: ", FLAGS.backend) 87 | print("Split: ", FLAGS.split) 88 | print("Config: ", FLAGS.datacfg) 89 | print("Limit: ", FLAGS.limit) 90 | print("Codalab: ", FLAGS.codalab) 91 | print("*" * 80) 92 | 93 | # assert split 94 | assert(FLAGS.split in splits) 95 | 96 | # assert backend 97 | assert(FLAGS.backend in backends) 98 | 99 | print("Opening data config file %s" % FLAGS.datacfg) 100 | DATA = yaml.safe_load(open(FLAGS.datacfg, 'r')) 101 | 102 | # get number of interest classes, and the label mappings 103 | class_strings = DATA["labels"] 104 | class_remap = DATA["learning_map"] 105 | class_inv_remap = DATA["learning_map_inv"] 106 | class_ignore = DATA["learning_ignore"] 107 | nr_classes = len(class_inv_remap) 108 | 109 | # make lookup table for mapping 110 | maxkey = max(class_remap.keys()) 111 | 112 | # +100 hack making lut bigger just in case there are unknown labels 113 | remap_lut = np.zeros((maxkey + 100), dtype=np.int32) 114 | remap_lut[list(class_remap.keys())] = list(class_remap.values()) 115 | # print(remap_lut) 116 | 117 | # create evaluator 118 | ignore = [] 119 | for cl, ign in class_ignore.items(): 120 | if ign: 121 | x_cl = int(cl) 122 | ignore.append(x_cl) 123 | print("Ignoring xentropy class ", x_cl, " in IoU evaluation") 124 | 125 | # create evaluator 126 | if FLAGS.backend == "torch": 127 | from auxiliary.torch_ioueval import iouEval 128 | evaluator = iouEval(nr_classes, ignore) 129 | elif FLAGS.backend == "numpy": 130 | from auxiliary.np_ioueval import iouEval 131 | evaluator = iouEval(nr_classes, ignore) 132 | else: 133 | print("Backend for evaluator should be one of ", str(backends)) 134 | quit() 135 | evaluator.reset() 136 | 137 | # get test set 138 | test_sequences = DATA["split"][FLAGS.split] 139 | 140 | # get label paths 141 | label_names = [] 142 | for sequence in test_sequences: 143 | sequence = '{0:02d}'.format(int(sequence)) 144 | label_paths = os.path.join(FLAGS.dataset, "sequences", 145 | str(sequence), "labels") 146 | # populate the label names 147 | seq_label_names = [os.path.join(dp, f) for dp, dn, fn in os.walk( 148 | os.path.expanduser(label_paths)) for f in fn if ".label" in f] 149 | seq_label_names.sort() 150 | label_names.extend(seq_label_names) 151 | # print(label_names) 152 | 153 | # get predictions paths 154 | pred_names = [] 155 | for sequence in test_sequences: 156 | sequence = '{0:02d}'.format(int(sequence)) 157 | pred_paths = os.path.join(FLAGS.predictions, "sequences", 158 | sequence, "predictions") 159 | # populate the label names 160 | seq_pred_names = [os.path.join(dp, f) for dp, dn, fn in os.walk( 161 | os.path.expanduser(pred_paths)) for f in fn if ".label" in f] 162 | seq_pred_names.sort() 163 | pred_names.extend(seq_pred_names) 164 | # print(pred_names) 165 | 166 | # check that I have the same number of files 167 | # print("labels: ", len(label_names)) 168 | # print("predictions: ", len(pred_names)) 169 | assert(len(label_names) == len(pred_names)) 170 | 171 | progress = 10 172 | count = 0 173 | print("Evaluating sequences: ", end="", flush=True) 174 | # open each file, get the tensor, and make the iou comparison 175 | for label_file, pred_file in zip(label_names, pred_names): 176 | count += 1 177 | if 100 * count / len(label_names) > progress: 178 | print("{:d}% ".format(progress), end="", flush=True) 179 | progress += 10 180 | 181 | # print("evaluating label ", label_file) 182 | # open label 183 | label = np.fromfile(label_file, dtype=np.int32) 184 | label = label.reshape((-1)) # reshape to vector 185 | label = label & 0xFFFF # get lower half for semantics 186 | if FLAGS.limit is not None: 187 | label = label[:FLAGS.limit] # limit to desired length 188 | label = remap_lut[label] # remap to xentropy format 189 | 190 | # open prediction 191 | pred = np.fromfile(pred_file, dtype=np.int32) 192 | pred = pred.reshape((-1)) # reshape to vector 193 | pred = pred & 0xFFFF # get lower half for semantics 194 | if FLAGS.limit is not None: 195 | pred = pred[:FLAGS.limit] # limit to desired length 196 | pred = remap_lut[pred] # remap to xentropy format 197 | 198 | # add single scan to evaluation 199 | evaluator.addBatch(pred, label) 200 | 201 | # when I am done, print the evaluation 202 | m_accuracy = evaluator.getacc() 203 | m_jaccard, class_jaccard = evaluator.getIoU() 204 | 205 | print('Validation set:\n' 206 | 'Acc avg {m_accuracy:.3f}\n' 207 | 'IoU avg {m_jaccard:.3f}'.format(m_accuracy=m_accuracy, 208 | m_jaccard=m_jaccard)) 209 | # print also classwise 210 | for i, jacc in enumerate(class_jaccard): 211 | if i not in ignore: 212 | print('IoU class {i:} [{class_str:}] = {jacc:.3f}'.format( 213 | i=i, class_str=class_strings[class_inv_remap[i]], jacc=jacc)) 214 | 215 | # print for spreadsheet 216 | print("*" * 80) 217 | print("below can be copied straight for paper table") 218 | for i, jacc in enumerate(class_jaccard): 219 | if i not in ignore: 220 | sys.stdout.write('{jacc:.3f}'.format(jacc=jacc.item())) 221 | sys.stdout.write(",") 222 | sys.stdout.write('{jacc:.3f}'.format(jacc=m_jaccard.item())) 223 | sys.stdout.write(",") 224 | sys.stdout.write('{acc:.3f}'.format(acc=m_accuracy.item())) 225 | sys.stdout.write('\n') 226 | sys.stdout.flush() 227 | 228 | # if codalab is necessary, then do it 229 | if FLAGS.codalab is not None: 230 | results = {} 231 | results["accuracy_mean"] = float(m_accuracy) 232 | results["iou_mean"] = float(m_jaccard) 233 | for i, jacc in enumerate(class_jaccard): 234 | if i not in ignore: 235 | results["iou_"+class_strings[class_inv_remap[i]]] = float(jacc) 236 | # save to file 237 | output_filename = os.path.join(FLAGS.codalab, 'scores.txt') 238 | with open(output_filename, 'w') as yaml_file: 239 | yaml.dump(results, yaml_file, default_flow_style=False) 240 | -------------------------------------------------------------------------------- /auxiliary/laserscanvis.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # This file is covered by the LICENSE file in the root of this project. 3 | 4 | import vispy 5 | from vispy.scene import visuals, SceneCanvas 6 | import numpy as np 7 | from matplotlib import pyplot as plt 8 | from auxiliary.laserscan import LaserScan, SemLaserScan 9 | 10 | 11 | class LaserScanVis: 12 | """Class that creates and handles a visualizer for a pointcloud""" 13 | 14 | def __init__(self, scan, scan_names, label_names, offset=0, 15 | semantics=True, instances=False, images=True, link=False): 16 | self.scan = scan 17 | self.scan_names = scan_names 18 | self.label_names = label_names 19 | self.offset = offset 20 | self.total = len(self.scan_names) 21 | self.semantics = semantics 22 | self.instances = instances 23 | self.images = images 24 | self.link = link 25 | # sanity check 26 | if not self.semantics and self.instances: 27 | print("Instances are only allowed in when semantics=True") 28 | raise ValueError 29 | 30 | self.reset() 31 | self.update_scan() 32 | 33 | def reset(self): 34 | """ Reset. """ 35 | # last key press (it should have a mutex, but visualization is not 36 | # safety critical, so let's do things wrong) 37 | self.action = "no" # no, next, back, quit are the possibilities 38 | 39 | # new canvas prepared for visualizing data 40 | self.canvas = SceneCanvas(keys='interactive', show=True) 41 | # interface (n next, b back, q quit, very simple) 42 | self.canvas.events.key_press.connect(self.key_press) 43 | self.canvas.events.draw.connect(self.draw) 44 | # grid 45 | self.grid = self.canvas.central_widget.add_grid() 46 | 47 | # laserscan part 48 | self.scan_view = vispy.scene.widgets.ViewBox( 49 | border_color='white', parent=self.canvas.scene) 50 | self.grid.add_widget(self.scan_view, 0, 0) 51 | self.scan_vis = visuals.Markers(antialias=0) 52 | self.scan_view.camera = 'turntable' 53 | self.scan_view.add(self.scan_vis) 54 | visuals.XYZAxis(parent=self.scan_view.scene) 55 | # add semantics 56 | if self.semantics: 57 | print("Using semantics in visualizer") 58 | self.sem_view = vispy.scene.widgets.ViewBox( 59 | border_color='white', parent=self.canvas.scene) 60 | self.grid.add_widget(self.sem_view, 0, 1) 61 | self.sem_vis = visuals.Markers(antialias=0) 62 | self.sem_view.camera = 'turntable' 63 | self.sem_view.add(self.sem_vis) 64 | visuals.XYZAxis(parent=self.sem_view.scene) 65 | if self.link: 66 | self.sem_view.camera.link(self.scan_view.camera) 67 | 68 | if self.instances: 69 | print("Using instances in visualizer") 70 | self.inst_view = vispy.scene.widgets.ViewBox( 71 | border_color='white', parent=self.canvas.scene) 72 | self.grid.add_widget(self.inst_view, 0, 2) 73 | self.inst_vis = visuals.Markers(antialias=0) 74 | self.inst_view.camera = 'turntable' 75 | self.inst_view.add(self.inst_vis) 76 | visuals.XYZAxis(parent=self.inst_view.scene) 77 | if self.link: 78 | self.inst_view.camera.link(self.scan_view.camera) 79 | 80 | # add a view for the depth 81 | if self.images: 82 | # img canvas size 83 | self.multiplier = 1 84 | self.canvas_W = 1024 85 | self.canvas_H = 64 86 | if self.semantics: 87 | self.multiplier += 1 88 | if self.instances: 89 | self.multiplier += 1 90 | 91 | # new canvas for img 92 | self.img_canvas = SceneCanvas(keys='interactive', show=True, 93 | size=(self.canvas_W, self.canvas_H * self.multiplier)) 94 | # grid 95 | self.img_grid = self.img_canvas.central_widget.add_grid() 96 | # interface (n next, b back, q quit, very simple) 97 | self.img_canvas.events.key_press.connect(self.key_press) 98 | self.img_canvas.events.draw.connect(self.draw) 99 | self.img_view = vispy.scene.widgets.ViewBox( 100 | border_color='white', parent=self.img_canvas.scene) 101 | self.img_grid.add_widget(self.img_view, 0, 0) 102 | self.img_vis = visuals.Image(cmap='viridis') 103 | self.img_view.add(self.img_vis) 104 | 105 | # add image semantics 106 | if self.semantics: 107 | self.sem_img_view = vispy.scene.widgets.ViewBox( 108 | border_color='white', parent=self.img_canvas.scene) 109 | self.img_grid.add_widget(self.sem_img_view, 1, 0) 110 | self.sem_img_vis = visuals.Image(cmap='viridis') 111 | self.sem_img_view.add(self.sem_img_vis) 112 | 113 | # add instances 114 | if self.instances: 115 | self.inst_img_view = vispy.scene.widgets.ViewBox( 116 | border_color='white', parent=self.img_canvas.scene) 117 | self.img_grid.add_widget(self.inst_img_view, 2, 0) 118 | self.inst_img_vis = visuals.Image(cmap='viridis') 119 | self.inst_img_view.add(self.inst_img_vis) 120 | if self.link: 121 | self.inst_view.camera.link(self.scan_view.camera) 122 | 123 | def get_mpl_colormap(self, cmap_name): 124 | cmap = plt.get_cmap(cmap_name) 125 | 126 | # Initialize the matplotlib color map 127 | sm = plt.cm.ScalarMappable(cmap=cmap) 128 | 129 | # Obtain linear color range 130 | color_range = sm.to_rgba(np.linspace(0, 1, 256), bytes=True)[:, 2::-1] 131 | 132 | return color_range.reshape(256, 3).astype(np.float32) / 255.0 133 | def update_scan(self): 134 | # first open data 135 | self.scan.open_scan(self.scan_names[self.offset]) 136 | if self.semantics: 137 | self.scan.open_label(self.label_names[self.offset]) 138 | self.scan.colorize() 139 | 140 | # then change names 141 | title = "scan " + str(self.offset) 142 | self.canvas.title = title 143 | if self.images: 144 | self.img_canvas.title = title 145 | 146 | # then do all the point cloud stuff 147 | 148 | # plot scan 149 | power = 16 150 | # print() 151 | range_data = np.copy(self.scan.unproj_range) 152 | # print(range_data.max(), range_data.min()) 153 | range_data = range_data**(1 / power) 154 | # print(range_data.max(), range_data.min()) 155 | viridis_range = ((range_data - range_data.min()) / 156 | (range_data.max() - range_data.min()) * 157 | 255).astype(np.uint8) 158 | viridis_map = self.get_mpl_colormap("viridis") 159 | viridis_colors = viridis_map[viridis_range] 160 | self.scan_vis.set_data(self.scan.points, 161 | face_color=viridis_colors[..., ::-1], 162 | edge_color=viridis_colors[..., ::-1], 163 | size=1) 164 | 165 | # plot semantics 166 | if self.semantics: 167 | self.sem_vis.set_data(self.scan.points, 168 | face_color=self.scan.sem_label_color[..., ::-1], 169 | edge_color=self.scan.sem_label_color[..., ::-1], 170 | size=1) 171 | 172 | # plot instances 173 | if self.instances: 174 | self.inst_vis.set_data(self.scan.points, 175 | face_color=self.scan.inst_label_color[..., ::-1], 176 | edge_color=self.scan.inst_label_color[..., ::-1], 177 | size=1) 178 | 179 | if self.images: 180 | # now do all the range image stuff 181 | # plot range image 182 | data = np.copy(self.scan.proj_range) 183 | # print(data[data > 0].max(), data[data > 0].min()) 184 | data[data > 0] = data[data > 0]**(1 / power) 185 | data[data < 0] = data[data > 0].min() 186 | # print(data.max(), data.min()) 187 | data = (data - data[data > 0].min()) / \ 188 | (data.max() - data[data > 0].min()) 189 | # print(data.max(), data.min()) 190 | self.img_vis.set_data(data) 191 | self.img_vis.update() 192 | 193 | if self.semantics: 194 | self.sem_img_vis.set_data(self.scan.proj_sem_color[..., ::-1]) 195 | self.sem_img_vis.update() 196 | 197 | if self.instances: 198 | self.inst_img_vis.set_data(self.scan.proj_inst_color[..., ::-1]) 199 | self.inst_img_vis.update() 200 | 201 | # interface 202 | def key_press(self, event): 203 | self.canvas.events.key_press.block() 204 | if self.images: 205 | self.img_canvas.events.key_press.block() 206 | if event.key == 'N': 207 | self.offset += 1 208 | if self.offset >= self.total: 209 | self.offset = 0 210 | self.update_scan() 211 | elif event.key == 'B': 212 | self.offset -= 1 213 | if self.offset < 0: 214 | self.offset = self.total - 1 215 | self.update_scan() 216 | elif event.key == 'Q' or event.key == 'Escape': 217 | self.destroy() 218 | 219 | def draw(self, event): 220 | if self.canvas.events.key_press.blocked(): 221 | self.canvas.events.key_press.unblock() 222 | if self.images and self.img_canvas.events.key_press.blocked(): 223 | self.img_canvas.events.key_press.unblock() 224 | 225 | def destroy(self): 226 | # destroy the visualization 227 | self.canvas.close() 228 | if self.images: 229 | self.img_canvas.close() 230 | vispy.app.quit() 231 | 232 | def run(self): 233 | vispy.app.run() 234 | -------------------------------------------------------------------------------- /evaluate_semantics_by_distance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # This file is covered by the LICENSE file in the root of this project. 3 | 4 | import argparse 5 | import os 6 | import yaml 7 | import sys 8 | import numpy as np 9 | 10 | DISTANCES = [(1e-8, 10.0), 11 | (10.0, 20.0), 12 | (20.0, 30.0), 13 | (30.0, 40.0), 14 | (40.0, 50.0)] 15 | 16 | # possible splits 17 | splits = ["train", "valid", "test"] 18 | 19 | # possible backends 20 | backends = ["numpy", "torch"] 21 | 22 | if __name__ == '__main__': 23 | parser = argparse.ArgumentParser("./evaluate_semantics_by_distance.py") 24 | parser.add_argument( 25 | '--dataset', '-d', 26 | type=str, 27 | required=True, 28 | help='Dataset dir. No Default', 29 | ) 30 | parser.add_argument( 31 | '--predictions', '-p', 32 | type=str, 33 | required=None, 34 | help='Prediction dir. Same organization as dataset, but predictions in' 35 | 'each sequences "prediction" directory. No Default. If no option is set' 36 | ' we look for the labels in the same directory as dataset' 37 | ) 38 | parser.add_argument( 39 | '--split', '-s', 40 | type=str, 41 | required=False, 42 | default="valid", 43 | help='Split to evaluate on. One of ' + 44 | str(splits) + '. Defaults to %(default)s', 45 | ) 46 | parser.add_argument( 47 | '--backend', '-b', 48 | type=str, 49 | required=False, 50 | default="numpy", 51 | help='Backend for evaluation. One of ' + 52 | str(backends) + ' Defaults to %(default)s', 53 | ) 54 | parser.add_argument( 55 | '--datacfg', '-dc', 56 | type=str, 57 | required=False, 58 | default="config/semantic-kitti.yaml", 59 | help='Dataset config file. Defaults to %(default)s', 60 | ) 61 | parser.add_argument( 62 | '--limit', '-l', 63 | type=int, 64 | required=False, 65 | default=None, 66 | help='Limit to the first "--limit" points of each scan. Useful for' 67 | ' evaluating single scan from agregated pointcloud.' 68 | ' Defaults to %(default)s', 69 | ) 70 | parser.add_argument( 71 | '--codalab', 72 | dest='codalab', 73 | default=False, 74 | action='store_true', 75 | help='Exports "segmentation_scores_distance.txt" for codalab' 76 | 'Defaults to %(default)s', 77 | ) 78 | FLAGS, unparsed = parser.parse_known_args() 79 | 80 | # fill in real predictions dir 81 | if FLAGS.predictions is None: 82 | FLAGS.predictions = FLAGS.dataset 83 | 84 | # print summary of what we will do 85 | print("*" * 80) 86 | print("INTERFACE:") 87 | print("Data: ", FLAGS.dataset) 88 | print("Predictions: ", FLAGS.predictions) 89 | print("Backend: ", FLAGS.backend) 90 | print("Split: ", FLAGS.split) 91 | print("Config: ", FLAGS.datacfg) 92 | print("Limit: ", FLAGS.limit) 93 | print("Codalab: ", FLAGS.codalab) 94 | print("*" * 80) 95 | 96 | # assert split 97 | assert(FLAGS.split in splits) 98 | 99 | # assert backend 100 | assert(FLAGS.backend in backends) 101 | 102 | print("Opening data config file %s" % FLAGS.datacfg) 103 | DATA = yaml.safe_load(open(FLAGS.datacfg, 'r')) 104 | 105 | # get number of interest classes, and the label mappings 106 | class_strings = DATA["labels"] 107 | class_remap = DATA["learning_map"] 108 | class_inv_remap = DATA["learning_map_inv"] 109 | class_ignore = DATA["learning_ignore"] 110 | nr_classes = len(class_inv_remap) 111 | 112 | # make lookup table for mapping 113 | maxkey = max(class_remap.keys()) 114 | 115 | # +100 hack making lut bigger just in case there are unknown labels 116 | remap_lut = np.zeros((maxkey + 100), dtype=np.int32) 117 | remap_lut[list(class_remap.keys())] = list(class_remap.values()) 118 | 119 | # print(remap_lut) 120 | 121 | # create evaluator 122 | ignore = [] 123 | for cl, ign in class_ignore.items(): 124 | if ign: 125 | x_cl = int(cl) 126 | ignore.append(x_cl) 127 | print("Ignoring xentropy class ", x_cl, " in IoU evaluation") 128 | 129 | # create evaluator 130 | evaluators = [] 131 | for i in range(len(DISTANCES)): 132 | if FLAGS.backend == "torch": 133 | from auxiliary.torch_ioueval import iouEval 134 | evaluators.append(iouEval(nr_classes, ignore)) 135 | evaluators[i].reset() 136 | elif FLAGS.backend == "numpy": 137 | from auxiliary.np_ioueval import iouEval 138 | evaluators.append(iouEval(nr_classes, ignore)) 139 | evaluators[i].reset() 140 | else: 141 | print("Backend for evaluator should be one of ", str(backends)) 142 | quit() 143 | 144 | # get test set 145 | test_sequences = DATA["split"][FLAGS.split] 146 | 147 | # get scan paths 148 | scan_names = [] 149 | for sequence in test_sequences: 150 | sequence = '{0:02d}'.format(int(sequence)) 151 | label_paths = os.path.join(FLAGS.dataset, "sequences", 152 | str(sequence), "velodyne") 153 | # populate the label names 154 | seq_scan_names = [os.path.join(dp, f) for dp, dn, fn in os.walk( 155 | os.path.expanduser(label_paths)) for f in fn if ".bin" in f] 156 | seq_scan_names.sort() 157 | scan_names.extend(seq_scan_names) 158 | # print(scan_names) 159 | 160 | # get label paths 161 | label_names = [] 162 | for sequence in test_sequences: 163 | sequence = '{0:02d}'.format(int(sequence)) 164 | label_paths = os.path.join(FLAGS.dataset, "sequences", 165 | str(sequence), "labels") 166 | # populate the label names 167 | seq_label_names = [os.path.join(dp, f) for dp, dn, fn in os.walk( 168 | os.path.expanduser(label_paths)) for f in fn if ".label" in f] 169 | seq_label_names.sort() 170 | label_names.extend(seq_label_names) 171 | # print(label_names) 172 | 173 | # get predictions paths 174 | pred_names = [] 175 | for sequence in test_sequences: 176 | sequence = '{0:02d}'.format(int(sequence)) 177 | pred_paths = os.path.join(FLAGS.predictions, "sequences", 178 | sequence, "predictions") 179 | # populate the label names 180 | seq_pred_names = [os.path.join(dp, f) for dp, dn, fn in os.walk( 181 | os.path.expanduser(pred_paths)) for f in fn if ".label" in f] 182 | seq_pred_names.sort() 183 | pred_names.extend(seq_pred_names) 184 | # print(pred_names) 185 | 186 | # check that I have the same number of files 187 | print("scans", len(scan_names)) 188 | print("labels: ", len(label_names)) 189 | print("predictions: ", len(pred_names)) 190 | assert(len(label_names) == len(pred_names) and 191 | len(scan_names) == len(label_names)) 192 | 193 | # open each file, get the tensor, and make the iou comparison 194 | for scan_file, label_file, pred_file in zip(scan_names, label_names, pred_names): 195 | print("evaluating scan ", scan_file) 196 | # open scan 197 | scan = np.fromfile(scan_file, dtype=np.float32) 198 | scan = scan.reshape((-1, 4)) # reshape to matrix 199 | if FLAGS.limit is not None: 200 | scan = scan[:FLAGS.limit] # limit to desired length 201 | depth = np.linalg.norm(scan[:, :3], 2, axis=1) # get depth to filter by distance 202 | 203 | # open label 204 | label = np.fromfile(label_file, dtype=np.int32) 205 | label = label.reshape((-1)) # reshape to vector 206 | label = label & 0xFFFF # get lower half for semantics 207 | if FLAGS.limit is not None: 208 | label = label[:FLAGS.limit] # limit to desired length 209 | label = remap_lut[label] # remap to xentropy format 210 | 211 | # open prediction 212 | pred = np.fromfile(pred_file, dtype=np.int32) 213 | pred = pred.reshape((-1)) # reshape to vector 214 | pred = pred & 0xFFFF # get lower half for semantics 215 | if FLAGS.limit is not None: 216 | pred = pred[:FLAGS.limit] # limit to desired length 217 | pred = remap_lut[pred] # remap to xentropy format 218 | 219 | # evaluate for all distances 220 | for idx in range(len(DISTANCES)): 221 | # select by range 222 | lrange = DISTANCES[idx][0] 223 | hrange = DISTANCES[idx][1] 224 | mask = np.logical_and(depth > lrange, depth < hrange) 225 | 226 | # mask by distance 227 | # mask_depth = depth[mask] 228 | # print("mask range, ", mask_depth.max(), mask_depth.min()) 229 | mask_label = label[mask] 230 | mask_pred = pred[mask] 231 | 232 | # add single scan to evaluation 233 | evaluators[idx].addBatch(mask_pred, mask_label) 234 | 235 | # print for all ranges 236 | print("*" * 80) 237 | for idx in range(len(DISTANCES)): 238 | # when I am done, print the evaluation 239 | m_accuracy = evaluators[idx].getacc() 240 | m_jaccard, class_jaccard = evaluators[idx].getIoU() 241 | 242 | # print for spreadsheet 243 | sys.stdout.write('range {lrange}m to {hrange}m,'.format(lrange=DISTANCES[idx][0], 244 | hrange=DISTANCES[idx][1])) 245 | for i, jacc in enumerate(class_jaccard): 246 | if i not in ignore: 247 | sys.stdout.write('{jacc:.3f}'.format(jacc=jacc.item())) 248 | sys.stdout.write(",") 249 | sys.stdout.write('{jacc:.3f}'.format(jacc=m_jaccard.item())) 250 | sys.stdout.write(",") 251 | sys.stdout.write('{acc:.3f}'.format(acc=m_accuracy.item())) 252 | sys.stdout.write('\n') 253 | sys.stdout.flush() 254 | 255 | # if codalab is necessary, then do it 256 | if FLAGS.codalab: 257 | results = {} 258 | for idx in range(len(DISTANCES)): 259 | # make string for distance 260 | d_str = str(DISTANCES[idx][-1])+"m_" 261 | 262 | # get values for this distance range 263 | m_accuracy = evaluators[idx].getacc() 264 | m_jaccard, class_jaccard = evaluators[idx].getIoU() 265 | 266 | # put in dictionary 267 | results[d_str+"accuracy_mean"] = float(m_accuracy) 268 | results[d_str+"iou_mean"] = float(m_jaccard) 269 | for i, jacc in enumerate(class_jaccard): 270 | if i not in ignore: 271 | results[d_str+"iou_"+class_strings[class_inv_remap[i]]] = float(jacc) 272 | # save to file 273 | with open('segmentation_scores_distance.txt', 'w') as yaml_file: 274 | yaml.dump(results, yaml_file, default_flow_style=False) 275 | -------------------------------------------------------------------------------- /auxiliary/laserscan.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import numpy as np 3 | 4 | 5 | class LaserScan: 6 | """Class that contains LaserScan with x,y,z,r""" 7 | EXTENSIONS_SCAN = ['.bin'] 8 | 9 | def __init__(self, project=False, H=64, W=1024, fov_up=3.0, fov_down=-25.0): 10 | self.project = project 11 | self.proj_H = H 12 | self.proj_W = W 13 | self.proj_fov_up = fov_up 14 | self.proj_fov_down = fov_down 15 | self.reset() 16 | 17 | def reset(self): 18 | """ Reset scan members. """ 19 | self.points = np.zeros((0, 3), dtype=np.float32) # [m, 3]: x, y, z 20 | self.remissions = np.zeros((0, 1), dtype=np.float32) # [m ,1]: remission 21 | 22 | # projected range image - [H,W] range (-1 is no data) 23 | self.proj_range = np.full((self.proj_H, self.proj_W), -1, 24 | dtype=np.float32) 25 | 26 | # unprojected range (list of depths for each point) 27 | self.unproj_range = np.zeros((0, 1), dtype=np.float32) 28 | 29 | # projected point cloud xyz - [H,W,3] xyz coord (-1 is no data) 30 | self.proj_xyz = np.full((self.proj_H, self.proj_W, 3), -1, 31 | dtype=np.float32) 32 | 33 | # projected remission - [H,W] intensity (-1 is no data) 34 | self.proj_remission = np.full((self.proj_H, self.proj_W), -1, 35 | dtype=np.float32) 36 | 37 | # projected index (for each pixel, what I am in the pointcloud) 38 | # [H,W] index (-1 is no data) 39 | self.proj_idx = np.full((self.proj_H, self.proj_W), -1, 40 | dtype=np.int32) 41 | 42 | # for each point, where it is in the range image 43 | self.proj_x = np.zeros((0, 1), dtype=np.float32) # [m, 1]: x 44 | self.proj_y = np.zeros((0, 1), dtype=np.float32) # [m, 1]: y 45 | 46 | # mask containing for each pixel, if it contains a point or not 47 | self.proj_mask = np.zeros((self.proj_H, self.proj_W), 48 | dtype=np.int32) # [H,W] mask 49 | 50 | def size(self): 51 | """ Return the size of the point cloud. """ 52 | return self.points.shape[0] 53 | 54 | def __len__(self): 55 | return self.size() 56 | 57 | def open_scan(self, filename): 58 | """ Open raw scan and fill in attributes 59 | """ 60 | # reset just in case there was an open structure 61 | self.reset() 62 | 63 | # check filename is string 64 | if not isinstance(filename, str): 65 | raise TypeError("Filename should be string type, " 66 | "but was {type}".format(type=str(type(filename)))) 67 | 68 | # check extension is a laserscan 69 | if not any(filename.endswith(ext) for ext in self.EXTENSIONS_SCAN): 70 | raise RuntimeError("Filename extension is not valid scan file.") 71 | 72 | # if all goes well, open pointcloud 73 | scan = np.fromfile(filename, dtype=np.float32) 74 | scan = scan.reshape((-1, 4)) 75 | 76 | # put in attribute 77 | points = scan[:, 0:3] # get xyz 78 | remissions = scan[:, 3] # get remission 79 | self.set_points(points, remissions) 80 | 81 | def set_points(self, points, remissions=None): 82 | """ Set scan attributes (instead of opening from file) 83 | """ 84 | # reset just in case there was an open structure 85 | self.reset() 86 | 87 | # check scan makes sense 88 | if not isinstance(points, np.ndarray): 89 | raise TypeError("Scan should be numpy array") 90 | 91 | # check remission makes sense 92 | if remissions is not None and not isinstance(remissions, np.ndarray): 93 | raise TypeError("Remissions should be numpy array") 94 | 95 | # put in attribute 96 | self.points = points # get xyz 97 | if remissions is not None: 98 | self.remissions = remissions # get remission 99 | else: 100 | self.remissions = np.zeros((points.shape[0]), dtype=np.float32) 101 | 102 | # if projection is wanted, then do it and fill in the structure 103 | if self.project: 104 | self.do_range_projection() 105 | 106 | def do_range_projection(self): 107 | """ Project a pointcloud into a spherical projection image.projection. 108 | Function takes no arguments because it can be also called externally 109 | if the value of the constructor was not set (in case you change your 110 | mind about wanting the projection) 111 | """ 112 | # laser parameters 113 | fov_up = self.proj_fov_up / 180.0 * np.pi # field of view up in rad 114 | fov_down = self.proj_fov_down / 180.0 * np.pi # field of view down in rad 115 | fov = abs(fov_down) + abs(fov_up) # get field of view total in rad 116 | 117 | # get depth of all points 118 | depth = np.linalg.norm(self.points, 2, axis=1) 119 | 120 | # get scan components 121 | scan_x = self.points[:, 0] 122 | scan_y = self.points[:, 1] 123 | scan_z = self.points[:, 2] 124 | 125 | # get angles of all points 126 | yaw = -np.arctan2(scan_y, scan_x) 127 | pitch = np.arcsin(scan_z / (depth + 1e-8)) 128 | 129 | # get projections in image coords 130 | proj_x = 0.5 * (yaw / np.pi + 1.0) # in [0.0, 1.0] 131 | proj_y = 1.0 - (pitch + abs(fov_down)) / fov # in [0.0, 1.0] 132 | 133 | # scale to image size using angular resolution 134 | proj_x *= self.proj_W # in [0.0, W] 135 | proj_y *= self.proj_H # in [0.0, H] 136 | 137 | # round and clamp for use as index 138 | proj_x = np.floor(proj_x) 139 | proj_x = np.minimum(self.proj_W - 1, proj_x) 140 | proj_x = np.maximum(0, proj_x).astype(np.int32) # in [0,W-1] 141 | self.proj_x = np.copy(proj_x) # store a copy in orig order 142 | 143 | proj_y = np.floor(proj_y) 144 | proj_y = np.minimum(self.proj_H - 1, proj_y) 145 | proj_y = np.maximum(0, proj_y).astype(np.int32) # in [0,H-1] 146 | self.proj_y = np.copy(proj_y) # stope a copy in original order 147 | 148 | # copy of depth in original order 149 | self.unproj_range = np.copy(depth) 150 | 151 | # order in decreasing depth 152 | indices = np.arange(depth.shape[0]) 153 | order = np.argsort(depth)[::-1] 154 | depth = depth[order] 155 | indices = indices[order] 156 | points = self.points[order] 157 | remission = self.remissions[order] 158 | proj_y = proj_y[order] 159 | proj_x = proj_x[order] 160 | 161 | # assing to images 162 | self.proj_range[proj_y, proj_x] = depth 163 | self.proj_xyz[proj_y, proj_x] = points 164 | self.proj_remission[proj_y, proj_x] = remission 165 | self.proj_idx[proj_y, proj_x] = indices 166 | self.proj_mask = (self.proj_idx > 0).astype(np.float32) 167 | 168 | 169 | class SemLaserScan(LaserScan): 170 | """Class that contains LaserScan with x,y,z,r,sem_label,sem_color_label,inst_label,inst_color_label""" 171 | EXTENSIONS_LABEL = ['.label'] 172 | 173 | def __init__(self, sem_color_dict=None, project=False, H=64, W=1024, fov_up=3.0, fov_down=-25.0): 174 | super(SemLaserScan, self).__init__(project, H, W, fov_up, fov_down) 175 | self.reset() 176 | 177 | # make semantic colors 178 | max_sem_key = 0 179 | for key, data in sem_color_dict.items(): 180 | if key + 1 > max_sem_key: 181 | max_sem_key = key + 1 182 | self.sem_color_lut = np.zeros((max_sem_key + 100, 3), dtype=np.float32) 183 | for key, value in sem_color_dict.items(): 184 | self.sem_color_lut[key] = np.array(value, np.float32) / 255.0 185 | 186 | # make instance colors 187 | max_inst_id = 100000 188 | self.inst_color_lut = np.random.uniform(low=0.0, 189 | high=1.0, 190 | size=(max_inst_id, 3)) 191 | # force zero to a gray-ish color 192 | self.inst_color_lut[0] = np.full((3), 0.1) 193 | 194 | def reset(self): 195 | """ Reset scan members. """ 196 | super(SemLaserScan, self).reset() 197 | 198 | # semantic labels 199 | self.sem_label = np.zeros((0, 1), dtype=np.uint32) # [m, 1]: label 200 | self.sem_label_color = np.zeros((0, 3), dtype=np.float32) # [m ,3]: color 201 | 202 | # instance labels 203 | self.inst_label = np.zeros((0, 1), dtype=np.uint32) # [m, 1]: label 204 | self.inst_label_color = np.zeros((0, 3), dtype=np.float32) # [m ,3]: color 205 | 206 | # projection color with semantic labels 207 | self.proj_sem_label = np.zeros((self.proj_H, self.proj_W), 208 | dtype=np.int32) # [H,W] label 209 | self.proj_sem_color = np.zeros((self.proj_H, self.proj_W, 3), 210 | dtype=float) # [H,W,3] color 211 | 212 | # projection color with instance labels 213 | self.proj_inst_label = np.zeros((self.proj_H, self.proj_W), 214 | dtype=np.int32) # [H,W] label 215 | self.proj_inst_color = np.zeros((self.proj_H, self.proj_W, 3), 216 | dtype=float) # [H,W,3] color 217 | 218 | def open_label(self, filename): 219 | """ Open raw scan and fill in attributes 220 | """ 221 | # check filename is string 222 | if not isinstance(filename, str): 223 | raise TypeError("Filename should be string type, " 224 | "but was {type}".format(type=str(type(filename)))) 225 | 226 | # check extension is a laserscan 227 | if not any(filename.endswith(ext) for ext in self.EXTENSIONS_LABEL): 228 | raise RuntimeError("Filename extension is not valid label file.") 229 | 230 | # if all goes well, open label 231 | label = np.fromfile(filename, dtype=np.uint32) 232 | label = label.reshape((-1)) 233 | 234 | # set it 235 | self.set_label(label) 236 | 237 | def set_label(self, label): 238 | """ Set points for label not from file but from np 239 | """ 240 | # check label makes sense 241 | if not isinstance(label, np.ndarray): 242 | raise TypeError("Label should be numpy array") 243 | 244 | # only fill in attribute if the right size 245 | if label.shape[0] == self.points.shape[0]: 246 | self.sem_label = label & 0xFFFF # semantic label in lower half 247 | self.inst_label = label >> 16 # instance id in upper half 248 | else: 249 | print("Points shape: ", self.points.shape) 250 | print("Label shape: ", label.shape) 251 | raise ValueError("Scan and Label don't contain same number of points") 252 | 253 | # sanity check 254 | assert((self.sem_label + (self.inst_label << 16) == label).all()) 255 | 256 | if self.project: 257 | self.do_label_projection() 258 | 259 | def colorize(self): 260 | """ Colorize pointcloud with the color of each semantic label 261 | """ 262 | self.sem_label_color = self.sem_color_lut[self.sem_label] 263 | self.sem_label_color = self.sem_label_color.reshape((-1, 3)) 264 | 265 | self.inst_label_color = self.inst_color_lut[self.inst_label] 266 | self.inst_label_color = self.inst_label_color.reshape((-1, 3)) 267 | 268 | def do_label_projection(self): 269 | # only map colors to labels that exist 270 | mask = self.proj_idx >= 0 271 | 272 | # semantics 273 | self.proj_sem_label[mask] = self.sem_label[self.proj_idx[mask]] 274 | self.proj_sem_color[mask] = self.sem_color_lut[self.sem_label[self.proj_idx[mask]]] 275 | 276 | # instances 277 | self.proj_inst_label[mask] = self.inst_label[self.proj_idx[mask]] 278 | self.proj_inst_color[mask] = self.inst_color_lut[self.inst_label[self.proj_idx[mask]]] 279 | -------------------------------------------------------------------------------- /evaluate_panoptic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # This file is covered by the LICENSE file in the root of this project. 3 | 4 | import argparse 5 | import os 6 | import yaml 7 | import sys 8 | import numpy as np 9 | import time 10 | import json 11 | 12 | from auxiliary.eval_np import PanopticEval 13 | 14 | # possible splits 15 | splits = ["train", "valid", "test"] 16 | 17 | if __name__ == '__main__': 18 | parser = argparse.ArgumentParser("./evaluate_panoptic.py") 19 | parser.add_argument( 20 | '--dataset', 21 | '-d', 22 | type=str, 23 | required=True, 24 | help='Dataset dir. No Default', 25 | ) 26 | parser.add_argument( 27 | '--predictions', 28 | '-p', 29 | type=str, 30 | required=None, 31 | help='Prediction dir. Same organization as dataset, but predictions in' 32 | 'each sequences "prediction" directory. No Default. If no option is set' 33 | ' we look for the labels in the same directory as dataset') 34 | parser.add_argument( 35 | '--split', 36 | '-s', 37 | type=str, 38 | required=False, 39 | choices=["train", "valid", "test"], 40 | default="valid", 41 | help='Split to evaluate on. One of ' + str(splits) + '. Defaults to %(default)s', 42 | ) 43 | parser.add_argument( 44 | '--data_cfg', 45 | '-dc', 46 | type=str, 47 | required=False, 48 | default="config/semantic-kitti.yaml", 49 | help='Dataset config file. Defaults to %(default)s', 50 | ) 51 | parser.add_argument( 52 | '--limit', 53 | '-l', 54 | type=int, 55 | required=False, 56 | default=None, 57 | help='Limit to the first "--limit" points of each scan. Useful for' 58 | ' evaluating single scan from aggregated pointcloud.' 59 | ' Defaults to %(default)s', 60 | ) 61 | parser.add_argument( 62 | '--min_inst_points', 63 | type=int, 64 | required=False, 65 | default=50, 66 | help='Lower bound for the number of points to be considered instance', 67 | ) 68 | parser.add_argument( 69 | '--output', 70 | type=str, 71 | required=False, 72 | default=None, 73 | help='Output directory for scores.txt and detailed_results.html.', 74 | ) 75 | 76 | start_time = time.time() 77 | 78 | FLAGS, unparsed = parser.parse_known_args() 79 | 80 | # fill in real predictions dir 81 | if FLAGS.predictions is None: 82 | FLAGS.predictions = FLAGS.dataset 83 | 84 | # print summary of what we will do 85 | print("*" * 80) 86 | print("INTERFACE:") 87 | print("Data: ", FLAGS.dataset) 88 | print("Predictions: ", FLAGS.predictions) 89 | print("Split: ", FLAGS.split) 90 | print("Config: ", FLAGS.data_cfg) 91 | print("Limit: ", FLAGS.limit) 92 | print("Min instance points: ", FLAGS.min_inst_points) 93 | print("Output directory", FLAGS.output) 94 | print("*" * 80) 95 | 96 | # assert split 97 | assert (FLAGS.split in splits) 98 | 99 | # open data config file 100 | DATA = yaml.safe_load(open(FLAGS.data_cfg, 'r')) 101 | 102 | # get number of interest classes, and the label mappings 103 | # class 104 | class_remap = DATA["learning_map"] 105 | class_inv_remap = DATA["learning_map_inv"] 106 | class_ignore = DATA["learning_ignore"] 107 | nr_classes = len(class_inv_remap) 108 | class_strings = DATA["labels"] 109 | 110 | # make lookup table for mapping 111 | # class 112 | maxkey = max(class_remap.keys()) 113 | 114 | # +100 hack making lut bigger just in case there are unknown labels 115 | class_lut = np.zeros((maxkey + 100), dtype=np.int32) 116 | class_lut[list(class_remap.keys())] = list(class_remap.values()) 117 | 118 | # class 119 | ignore_class = [cl for cl, ignored in class_ignore.items() if ignored] 120 | 121 | print("Ignoring classes: ", ignore_class) 122 | 123 | # create evaluator 124 | class_evaluator = PanopticEval(nr_classes, None, ignore_class, min_points=FLAGS.min_inst_points) 125 | 126 | # get test set 127 | test_sequences = DATA["split"][FLAGS.split] 128 | 129 | # get label paths 130 | label_names = [] 131 | for sequence in test_sequences: 132 | sequence = '{0:02d}'.format(int(sequence)) 133 | label_paths = os.path.join(FLAGS.dataset, "sequences", sequence, "labels") 134 | # populate the label names 135 | seq_label_names = sorted([os.path.join(label_paths, fn) for fn in os.listdir(label_paths) if fn.endswith(".label")]) 136 | label_names.extend(seq_label_names) 137 | # print(label_names) 138 | 139 | # get predictions paths 140 | pred_names = [] 141 | for sequence in test_sequences: 142 | sequence = '{0:02d}'.format(int(sequence)) 143 | pred_paths = os.path.join(FLAGS.predictions, "sequences", sequence, "predictions") 144 | # populate the label names 145 | seq_pred_names = sorted([os.path.join(pred_paths, fn) for fn in os.listdir(pred_paths) if fn.endswith(".label")]) 146 | pred_names.extend(seq_pred_names) 147 | # print(pred_names) 148 | 149 | # check that I have the same number of files 150 | assert (len(label_names) == len(pred_names)) 151 | 152 | print("Evaluating sequences: ", end="", flush=True) 153 | # open each file, get the tensor, and make the iou comparison 154 | 155 | complete = len(label_names) 156 | count = 0 157 | percent = 10 158 | for label_file, pred_file in zip(label_names, pred_names): 159 | count = count + 1 160 | if 100 * count / complete > percent: 161 | print("{}% ".format(percent), end="", flush=True) 162 | percent = percent + 10 163 | # print("evaluating label ", label_file, "with", pred_file) 164 | # open label 165 | 166 | label = np.fromfile(label_file, dtype=np.uint32) 167 | 168 | u_label_sem_class = class_lut[label & 0xFFFF] # remap to xentropy format 169 | u_label_inst = label # unique instance ids. 170 | if FLAGS.limit is not None: 171 | u_label_sem_class = u_label_sem_class[:FLAGS.limit] 172 | u_label_sem_cat = u_label_sem_cat[:FLAGS.limit] 173 | u_label_inst = u_label_inst[:FLAGS.limit] 174 | 175 | label = np.fromfile(pred_file, dtype=np.uint32) 176 | 177 | u_pred_sem_class = class_lut[label & 0xFFFF] # remap to xentropy format 178 | u_pred_inst = label # unique instance ids. 179 | if FLAGS.limit is not None: 180 | u_pred_sem_class = u_pred_sem_class[:FLAGS.limit] 181 | u_pred_sem_cat = u_pred_sem_cat[:FLAGS.limit] 182 | u_pred_inst = u_pred_inst[:FLAGS.limit] 183 | 184 | class_evaluator.addBatch(u_pred_sem_class, u_pred_inst, u_label_sem_class, u_label_inst) 185 | 186 | print("100%") 187 | 188 | complete_time = time.time() - start_time 189 | 190 | # when I am done, print the evaluation 191 | class_PQ, class_SQ, class_RQ, class_all_PQ, class_all_SQ, class_all_RQ = class_evaluator.getPQ() 192 | class_IoU, class_all_IoU = class_evaluator.getSemIoU() 193 | 194 | # now make a nice dictionary 195 | output_dict = {} 196 | 197 | # make python variables 198 | class_PQ = class_PQ.item() 199 | class_SQ = class_SQ.item() 200 | class_RQ = class_RQ.item() 201 | class_all_PQ = class_all_PQ.flatten().tolist() 202 | class_all_SQ = class_all_SQ.flatten().tolist() 203 | class_all_RQ = class_all_RQ.flatten().tolist() 204 | class_IoU = class_IoU.item() 205 | class_all_IoU = class_all_IoU.flatten().tolist() 206 | 207 | # fill in with the raw values 208 | # output_dict["raw"] = {} 209 | # output_dict["raw"]["class_PQ"] = class_PQ 210 | # output_dict["raw"]["class_SQ"] = class_SQ 211 | # output_dict["raw"]["class_RQ"] = class_RQ 212 | # output_dict["raw"]["class_all_PQ"] = class_all_PQ 213 | # output_dict["raw"]["class_all_SQ"] = class_all_SQ 214 | # output_dict["raw"]["class_all_RQ"] = class_all_RQ 215 | # output_dict["raw"]["class_IoU"] = class_IoU 216 | # output_dict["raw"]["class_all_IoU"] = class_all_IoU 217 | 218 | things = ['car', 'truck', 'bicycle', 'motorcycle', 'other-vehicle', 'person', 'bicyclist', 'motorcyclist'] 219 | stuff = [ 220 | 'road', 'sidewalk', 'parking', 'other-ground', 'building', 'vegetation', 'trunk', 'terrain', 'fence', 'pole', 221 | 'traffic-sign' 222 | ] 223 | all_classes = things + stuff 224 | 225 | # class 226 | 227 | output_dict["all"] = {} 228 | output_dict["all"]["PQ"] = class_PQ 229 | output_dict["all"]["SQ"] = class_SQ 230 | output_dict["all"]["RQ"] = class_RQ 231 | output_dict["all"]["IoU"] = class_IoU 232 | 233 | classwise_tables = {} 234 | 235 | for idx, (pq, rq, sq, iou) in enumerate(zip(class_all_PQ, class_all_RQ, class_all_SQ, class_all_IoU)): 236 | class_str = class_strings[class_inv_remap[idx]] 237 | output_dict[class_str] = {} 238 | output_dict[class_str]["PQ"] = pq 239 | output_dict[class_str]["SQ"] = sq 240 | output_dict[class_str]["RQ"] = rq 241 | output_dict[class_str]["IoU"] = iou 242 | 243 | PQ_all = np.mean([float(output_dict[c]["PQ"]) for c in all_classes]) 244 | PQ_dagger = np.mean([float(output_dict[c]["PQ"]) for c in things] + [float(output_dict[c]["IoU"]) for c in stuff]) 245 | RQ_all = np.mean([float(output_dict[c]["RQ"]) for c in all_classes]) 246 | SQ_all = np.mean([float(output_dict[c]["SQ"]) for c in all_classes]) 247 | 248 | PQ_things = np.mean([float(output_dict[c]["PQ"]) for c in things]) 249 | RQ_things = np.mean([float(output_dict[c]["RQ"]) for c in things]) 250 | SQ_things = np.mean([float(output_dict[c]["SQ"]) for c in things]) 251 | 252 | PQ_stuff = np.mean([float(output_dict[c]["PQ"]) for c in stuff]) 253 | RQ_stuff = np.mean([float(output_dict[c]["RQ"]) for c in stuff]) 254 | SQ_stuff = np.mean([float(output_dict[c]["SQ"]) for c in stuff]) 255 | mIoU = output_dict["all"]["IoU"] 256 | 257 | codalab_output = {} 258 | codalab_output["pq_mean"] = float(PQ_all) 259 | codalab_output["pq_dagger"] = float(PQ_dagger) 260 | codalab_output["sq_mean"] = float(SQ_all) 261 | codalab_output["rq_mean"] = float(RQ_all) 262 | codalab_output["iou_mean"] = float(mIoU) 263 | codalab_output["pq_stuff"] = float(PQ_stuff) 264 | codalab_output["rq_stuff"] = float(RQ_stuff) 265 | codalab_output["sq_stuff"] = float(SQ_stuff) 266 | codalab_output["pq_things"] = float(PQ_things) 267 | codalab_output["rq_things"] = float(RQ_things) 268 | codalab_output["sq_things"] = float(SQ_things) 269 | 270 | print("Completed in {} s".format(complete_time)) 271 | 272 | if FLAGS.output is not None: 273 | table = [] 274 | for cl in all_classes: 275 | entry = output_dict[cl] 276 | table.append({ 277 | "class": cl, 278 | "pq": "{:.3}".format(entry["PQ"]), 279 | "sq": "{:.3}".format(entry["SQ"]), 280 | "rq": "{:.3}".format(entry["RQ"]), 281 | "iou": "{:.3}".format(entry["IoU"]) 282 | }) 283 | 284 | print("Generating output files.") 285 | # save to yaml 286 | output_filename = os.path.join(FLAGS.output, 'scores.txt') 287 | with open(output_filename, 'w') as outfile: 288 | yaml.dump(codalab_output, outfile, default_flow_style=False) 289 | 290 | ## producing a detailed result page. 291 | output_filename = os.path.join(FLAGS.output, "detailed_results.html") 292 | with open(output_filename, "w") as html_file: 293 | html_file.write(""" 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 |
302 | 303 | 317 | 318 | """) 319 | -------------------------------------------------------------------------------- /auxiliary/eval_np.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # This file is covered by the LICENSE file in the root of this project. 4 | 5 | import numpy as np 6 | import time 7 | 8 | 9 | class PanopticEval: 10 | """ Panoptic evaluation using numpy 11 | 12 | authors: Andres Milioto and Jens Behley 13 | 14 | """ 15 | 16 | def __init__(self, n_classes, device=None, ignore=None, offset=2**32, min_points=30): 17 | self.n_classes = n_classes 18 | assert (device == None) 19 | self.ignore = np.array(ignore, dtype=np.int64) 20 | self.include = np.array([n for n in range(self.n_classes) if n not in self.ignore], dtype=np.int64) 21 | 22 | print("[PANOPTIC EVAL] IGNORE: ", self.ignore) 23 | print("[PANOPTIC EVAL] INCLUDE: ", self.include) 24 | 25 | self.reset() 26 | self.offset = offset # largest number of instances in a given scan 27 | self.min_points = min_points # smallest number of points to consider instances in gt 28 | self.eps = 1e-15 29 | 30 | def num_classes(self): 31 | return self.n_classes 32 | 33 | def reset(self): 34 | # general things 35 | # iou stuff 36 | self.px_iou_conf_matrix = np.zeros((self.n_classes, self.n_classes), dtype=np.int64) 37 | # panoptic stuff 38 | self.pan_tp = np.zeros(self.n_classes, dtype=np.int64) 39 | self.pan_iou = np.zeros(self.n_classes, dtype=np.double) 40 | self.pan_fp = np.zeros(self.n_classes, dtype=np.int64) 41 | self.pan_fn = np.zeros(self.n_classes, dtype=np.int64) 42 | 43 | ################################# IoU STUFF ################################## 44 | def addBatchSemIoU(self, x_sem, y_sem): 45 | # idxs are labels and predictions 46 | idxs = np.stack([x_sem, y_sem], axis=0) 47 | 48 | # make confusion matrix (cols = gt, rows = pred) 49 | np.add.at(self.px_iou_conf_matrix, tuple(idxs), 1) 50 | 51 | def getSemIoUStats(self): 52 | # clone to avoid modifying the real deal 53 | conf = self.px_iou_conf_matrix.copy().astype(np.double) 54 | # remove fp from confusion on the ignore classes predictions 55 | # points that were predicted of another class, but were ignore 56 | # (corresponds to zeroing the cols of those classes, since the predictions 57 | # go on the rows) 58 | conf[:, self.ignore] = 0 59 | 60 | # get the clean stats 61 | tp = conf.diagonal() 62 | fp = conf.sum(axis=1) - tp 63 | fn = conf.sum(axis=0) - tp 64 | return tp, fp, fn 65 | 66 | def getSemIoU(self): 67 | tp, fp, fn = self.getSemIoUStats() 68 | # print(f"tp={tp}") 69 | # print(f"fp={fp}") 70 | # print(f"fn={fn}") 71 | intersection = tp 72 | union = tp + fp + fn 73 | union = np.maximum(union, self.eps) 74 | iou = intersection.astype(np.double) / union.astype(np.double) 75 | iou_mean = (intersection[self.include].astype(np.double) / union[self.include].astype(np.double)).mean() 76 | 77 | return iou_mean, iou # returns "iou mean", "iou per class" ALL CLASSES 78 | 79 | def getSemAcc(self): 80 | tp, fp, fn = self.getSemIoUStats() 81 | total_tp = tp.sum() 82 | total = tp[self.include].sum() + fp[self.include].sum() 83 | total = np.maximum(total, self.eps) 84 | acc_mean = total_tp.astype(np.double) / total.astype(np.double) 85 | 86 | return acc_mean # returns "acc mean" 87 | 88 | ################################# IoU STUFF ################################## 89 | ############################################################################## 90 | 91 | ############################# Panoptic STUFF ################################ 92 | def addBatchPanoptic(self, x_sem_row, x_inst_row, y_sem_row, y_inst_row): 93 | # make sure instances are not zeros (it messes with my approach) 94 | x_inst_row = x_inst_row + 1 95 | y_inst_row = y_inst_row + 1 96 | 97 | # only interested in points that are outside the void area (not in excluded classes) 98 | for cl in self.ignore: 99 | # make a mask for this class 100 | gt_not_in_excl_mask = y_sem_row != cl 101 | # remove all other points 102 | x_sem_row = x_sem_row[gt_not_in_excl_mask] 103 | y_sem_row = y_sem_row[gt_not_in_excl_mask] 104 | x_inst_row = x_inst_row[gt_not_in_excl_mask] 105 | y_inst_row = y_inst_row[gt_not_in_excl_mask] 106 | 107 | # first step is to count intersections > 0.5 IoU for each class (except the ignored ones) 108 | for cl in self.include: 109 | # print("*"*80) 110 | # print("CLASS", cl.item()) 111 | # get a class mask 112 | x_inst_in_cl_mask = x_sem_row == cl 113 | y_inst_in_cl_mask = y_sem_row == cl 114 | 115 | # get instance points in class (makes outside stuff 0) 116 | x_inst_in_cl = x_inst_row * x_inst_in_cl_mask.astype(np.int64) 117 | y_inst_in_cl = y_inst_row * y_inst_in_cl_mask.astype(np.int64) 118 | 119 | # generate the areas for each unique instance prediction 120 | unique_pred, counts_pred = np.unique(x_inst_in_cl[x_inst_in_cl > 0], return_counts=True) 121 | id2idx_pred = {id: idx for idx, id in enumerate(unique_pred)} 122 | matched_pred = np.array([False] * unique_pred.shape[0]) 123 | # print("Unique predictions:", unique_pred) 124 | 125 | # generate the areas for each unique instance gt_np 126 | unique_gt, counts_gt = np.unique(y_inst_in_cl[y_inst_in_cl > 0], return_counts=True) 127 | id2idx_gt = {id: idx for idx, id in enumerate(unique_gt)} 128 | matched_gt = np.array([False] * unique_gt.shape[0]) 129 | # print("Unique ground truth:", unique_gt) 130 | 131 | # generate intersection using offset 132 | valid_combos = np.logical_and(x_inst_in_cl > 0, y_inst_in_cl > 0) 133 | offset_combo = x_inst_in_cl[valid_combos] + self.offset * y_inst_in_cl[valid_combos] 134 | unique_combo, counts_combo = np.unique(offset_combo, return_counts=True) 135 | 136 | # generate an intersection map 137 | # count the intersections with over 0.5 IoU as TP 138 | gt_labels = unique_combo // self.offset 139 | pred_labels = unique_combo % self.offset 140 | gt_areas = np.array([counts_gt[id2idx_gt[id]] for id in gt_labels]) 141 | pred_areas = np.array([counts_pred[id2idx_pred[id]] for id in pred_labels]) 142 | intersections = counts_combo 143 | unions = gt_areas + pred_areas - intersections 144 | ious = intersections.astype(float) / unions.astype(float) 145 | 146 | 147 | tp_indexes = ious > 0.5 148 | self.pan_tp[cl] += np.sum(tp_indexes) 149 | self.pan_iou[cl] += np.sum(ious[tp_indexes]) 150 | 151 | matched_gt[[id2idx_gt[id] for id in gt_labels[tp_indexes]]] = True 152 | matched_pred[[id2idx_pred[id] for id in pred_labels[tp_indexes]]] = True 153 | 154 | # count the FN 155 | self.pan_fn[cl] += np.sum(np.logical_and(counts_gt >= self.min_points, matched_gt == False)) 156 | 157 | # count the FP 158 | self.pan_fp[cl] += np.sum(np.logical_and(counts_pred >= self.min_points, matched_pred == False)) 159 | 160 | def getPQ(self): 161 | # first calculate for all classes 162 | sq_all = self.pan_iou.astype(np.double) / np.maximum(self.pan_tp.astype(np.double), self.eps) 163 | rq_all = self.pan_tp.astype(np.double) / np.maximum( 164 | self.pan_tp.astype(np.double) + 0.5 * self.pan_fp.astype(np.double) + 0.5 * self.pan_fn.astype(np.double), 165 | self.eps) 166 | pq_all = sq_all * rq_all 167 | 168 | # then do the REAL mean (no ignored classes) 169 | SQ = sq_all[self.include].mean() 170 | RQ = rq_all[self.include].mean() 171 | PQ = pq_all[self.include].mean() 172 | 173 | return PQ, SQ, RQ, pq_all, sq_all, rq_all 174 | 175 | ############################# Panoptic STUFF ################################ 176 | ############################################################################## 177 | 178 | def addBatch(self, x_sem, x_inst, y_sem, y_inst): # x=preds, y=targets 179 | ''' IMPORTANT: Inputs must be batched. Either [N,H,W], or [N, P] 180 | ''' 181 | # add to IoU calculation (for checking purposes) 182 | self.addBatchSemIoU(x_sem, y_sem) 183 | 184 | # now do the panoptic stuff 185 | self.addBatchPanoptic(x_sem, x_inst, y_sem, y_inst) 186 | 187 | 188 | if __name__ == "__main__": 189 | # generate problem from He paper (https://arxiv.org/pdf/1801.00868.pdf) 190 | classes = 5 # ignore, grass, sky, person, dog 191 | cl_strings = ["ignore", "grass", "sky", "person", "dog"] 192 | ignore = [0] # only ignore ignore class 193 | min_points = 1 # for this example we care about all points 194 | 195 | # generate ground truth and prediction 196 | sem_pred = [] 197 | inst_pred = [] 198 | sem_gt = [] 199 | inst_gt = [] 200 | 201 | # some ignore stuff 202 | N_ignore = 50 203 | sem_pred.extend([0 for i in range(N_ignore)]) 204 | inst_pred.extend([0 for i in range(N_ignore)]) 205 | sem_gt.extend([0 for i in range(N_ignore)]) 206 | inst_gt.extend([0 for i in range(N_ignore)]) 207 | 208 | # grass segment 209 | N_grass = 50 210 | N_grass_pred = 40 # rest is sky 211 | sem_pred.extend([1 for i in range(N_grass_pred)]) # grass 212 | sem_pred.extend([2 for i in range(N_grass - N_grass_pred)]) # sky 213 | inst_pred.extend([0 for i in range(N_grass)]) 214 | sem_gt.extend([1 for i in range(N_grass)]) # grass 215 | inst_gt.extend([0 for i in range(N_grass)]) 216 | 217 | # sky segment 218 | N_sky = 50 219 | N_sky_pred = 40 # rest is grass 220 | sem_pred.extend([2 for i in range(N_sky_pred)]) # sky 221 | sem_pred.extend([1 for i in range(N_sky - N_sky_pred)]) # grass 222 | inst_pred.extend([0 for i in range(N_sky)]) # first instance 223 | sem_gt.extend([2 for i in range(N_sky)]) # sky 224 | inst_gt.extend([0 for i in range(N_sky)]) # first instance 225 | 226 | # wrong dog as person prediction 227 | N_dog = 50 228 | N_person = N_dog 229 | sem_pred.extend([3 for i in range(N_person)]) 230 | inst_pred.extend([35 for i in range(N_person)]) 231 | sem_gt.extend([4 for i in range(N_dog)]) 232 | inst_gt.extend([22 for i in range(N_dog)]) 233 | 234 | # two persons in prediction, but three in gt 235 | N_person = 50 236 | sem_pred.extend([3 for i in range(6 * N_person)]) 237 | inst_pred.extend([8 for i in range(4 * N_person)]) 238 | inst_pred.extend([95 for i in range(2 * N_person)]) 239 | sem_gt.extend([3 for i in range(6 * N_person)]) 240 | inst_gt.extend([33 for i in range(3 * N_person)]) 241 | inst_gt.extend([42 for i in range(N_person)]) 242 | inst_gt.extend([11 for i in range(2 * N_person)]) 243 | 244 | # gt and pred to numpy 245 | sem_pred = np.array(sem_pred, dtype=np.int64).reshape(1, -1) 246 | inst_pred = np.array(inst_pred, dtype=np.int64).reshape(1, -1) 247 | sem_gt = np.array(sem_gt, dtype=np.int64).reshape(1, -1) 248 | inst_gt = np.array(inst_gt, dtype=np.int64).reshape(1, -1) 249 | 250 | # evaluator 251 | evaluator = PanopticEval(classes, ignore=ignore, min_points=1) 252 | evaluator.addBatch(sem_pred, inst_pred, sem_gt, inst_gt) 253 | pq, sq, rq, all_pq, all_sq, all_rq = evaluator.getPQ() 254 | iou, all_iou = evaluator.getSemIoU() 255 | 256 | # [PANOPTIC EVAL] IGNORE: [0] 257 | # [PANOPTIC EVAL] INCLUDE: [1 2 3 4] 258 | # TOTALS 259 | # PQ: 0.47916666666666663 260 | # SQ: 0.5520833333333333 261 | # RQ: 0.6666666666666666 262 | # IoU: 0.5476190476190476 263 | # Class ignore PQ: 0.0 SQ: 0.0 RQ: 0.0 IoU: 0.0 264 | # Class grass PQ: 0.6666666666666666 SQ: 0.6666666666666666 RQ: 1.0 IoU: 0.6666666666666666 265 | # Class sky PQ: 0.6666666666666666 SQ: 0.6666666666666666 RQ: 1.0 IoU: 0.6666666666666666 266 | # Class person PQ: 0.5833333333333333 SQ: 0.875 RQ: 0.6666666666666666 IoU: 0.8571428571428571 267 | # Class dog PQ: 0.0 SQ: 0.0 RQ: 0.0 IoU: 0.0 268 | 269 | print("TOTALS") 270 | print("PQ:", pq.item(), pq.item() == 0.47916666666666663) 271 | print("SQ:", sq.item(), sq.item() == 0.5520833333333333) 272 | print("RQ:", rq.item(), rq.item() == 0.6666666666666666) 273 | print("IoU:", iou.item(), iou.item() == 0.5476190476190476) 274 | for i, (pq, sq, rq, iou) in enumerate(zip(all_pq, all_sq, all_rq, all_iou)): 275 | print("Class", cl_strings[i], "\t", "PQ:", pq.item(), "SQ:", sq.item(), "RQ:", rq.item(), "IoU:", iou.item()) 276 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # API for SemanticKITTI 2 | 3 | This repository contains helper scripts to open, visualize, process, and 4 | evaluate results for point clouds and labels from the SemanticKITTI dataset. 5 | 6 | - Link to original [KITTI Odometry Benchmark](http://www.cvlibs.net/datasets/kitti/eval_odometry.php) Dataset 7 | - Link to [SemanticKITTI dataset](http://semantic-kitti.org/). 8 | - Link to SemanticKITTI benchmark [competition](http://semantic-kitti.org/tasks.html). 9 | 10 | --- 11 | ##### Example of 3D pointcloud from sequence 13: 12 | 13 | 14 | --- 15 | 16 | ##### Example of 2D spherical projection from sequence 13: 17 | 18 | 19 | --- 20 | 21 | ##### Example of voxelized point clouds for semantic scene completion: 22 | 23 | 24 | --- 25 | 26 | ## Data organization 27 | 28 | The data is organized in the following format: 29 | 30 | ``` 31 | /kitti/dataset/ 32 | └── sequences/ 33 | ├── 00/ 34 | │   ├── poses.txt 35 | │ ├── image_2/ 36 | │   ├── image_3/ 37 | │   ├── labels/ 38 | │ │ ├ 000000.label 39 | │ │ └ 000001.label 40 | | ├── voxels/ 41 | | | ├ 000000.bin 42 | | | ├ 000000.label 43 | | | ├ 000000.occluded 44 | | | ├ 000000.invalid 45 | | | ├ 000001.bin 46 | | | ├ 000001.label 47 | | | ├ 000001.occluded 48 | | | ├ 000001.invalid 49 | │   └── velodyne/ 50 | │ ├ 000000.bin 51 | │ └ 000001.bin 52 | ├── 01/ 53 | ├── 02/ 54 | . 55 | . 56 | . 57 | └── 21/ 58 | ``` 59 | 60 | - From [KITTI Odometry](http://www.cvlibs.net/datasets/kitti/eval_odometry.php): 61 | - `image_2` and `image_3` correspond to the rgb images for each sequence. 62 | - `velodyne` contains the pointclouds for each scan in each sequence. Each 63 | `.bin` scan is a list of float32 points in [x,y,z,remission] format. See 64 | [laserscan.py](auxiliary/laserscan.py) to see how the points are read. 65 | - From SemanticKITTI: 66 | - `labels` contains the labels for each scan in each sequence. Each `.label` 67 | file contains a uint32 label for each point in the corresponding `.bin` scan. 68 | See [laserscan.py](auxiliary/laserscan.py) to see how the labels are read. 69 | - `poses.txt` contain the manually looped-closed poses for each capture (in 70 | the camera frame) that were used in the annotation tools to aggregate all 71 | the point clouds. 72 | - `voxels` contains all information needed for the task of semantic scene completion. Each `.bin` file contains for each voxel if that voxel is occupied by laser measurements in a packed binary format. This is the input to the semantic scene completion task and it corresponds to the voxelization of a single LiDAR scan. Each`.label` file contains for each voxel of the completed scene a label in binary format. The label is a 16-bit unsigned integer (aka uint16_t) for each voxel. `.invalid` and `.occluded` contain information about the occlusion of voxel. Invalid voxels are voxels that are occluded from each view position and occluded voxels are occluded in the first view point. See also [SSCDataset.py](auxiliary/SSCDataset.py) for more information on loading the data. 73 | 74 | The main configuration file for the data is in `config/semantic-kitti.yaml`. In this file you will find: 75 | 76 | - `labels`: dictionary which maps numeric labels in `.label` file to a string class. Example: `10: "car"` 77 | - `color_map`: dictionary which maps numeric labels in `.label` file to a bgr color for visualization. Example `10: [245, 150, 100] # car, blue-ish` 78 | - `content`: dictionary with content of each class in labels, as a ratio to 79 | the number of total points in the dataset. This can be obtained by running the 80 | [./content.py](./content.py) script, and is used to calculate the weights for the cross 81 | entropy in all baseline methods (in order handle class imbalance). 82 | - `learning_map`: dictionary which maps each class label to its cross entropy 83 | equivalent, for learning. This is done to mask undesired classes, map different 84 | classes together, and because the cross entropy expects a value in 85 | [0, numclasses - 1]. We also provide [./remap_semantic_labels.py](./remap_semantic_labels.py), 86 | a script that uses this dictionary to put the label files in the cross entropy format, 87 | so that you can use the labels directly in your training pipeline. 88 | Examples: 89 | ```yaml 90 | 0 : 0 # "unlabeled" 91 | 1 : 0 # "outlier" to "unlabeled" -> gets ignored in training, with unlabeled 92 | 10: 1 # "car" 93 | 252: 1 # "moving-car" to "car" -> gets merged with static car class 94 | ``` 95 | - `learning_map_inv`: dictionary with inverse of the previous mapping, allows to 96 | map back the classes only to the interest ones (for saving point cloud predictions 97 | in original label format). We also provide [./remap_semantic_labels.py](./remap_semantic_labels.py), 98 | a script that uses this dictionary to put the label files in the original format, 99 | when instantiated with the `--inverse` flag. 100 | - `learning_ignore`: dictionary that contains for each cross entropy class if it 101 | will be ignored during training and evaluation or not. For example, class `unlabeled` gets 102 | ignored in both training and evaluation. 103 | - `split`: contains 3 lists, with the sequence numbers for training, validation, and evaluation. 104 | 105 | 106 | ## Dependencies for API: 107 | 108 | System dependencies 109 | 110 | ```sh 111 | $ sudo apt install python3-dev python3-pip python3-pyqt5.qtopengl # for visualization 112 | ``` 113 | 114 | Python dependencies 115 | 116 | ```sh 117 | $ sudo pip3 install -r requirements.txt 118 | ``` 119 | 120 | ## Scripts: 121 | 122 | **ALL OF THE SCRIPTS CAN BE INVOKED WITH THE --help (-h) FLAG, FOR EXTRA INFORMATION AND OPTIONS.** 123 | 124 | #### Visualization 125 | 126 | 127 | ##### Point Clouds 128 | 129 | To visualize the data, use the `visualize.py` script. It will open an interactive 130 | opengl visualization of the pointclouds along with a spherical projection of 131 | each scan into a 64 x 1024 image. 132 | 133 | ```sh 134 | $ ./visualize.py --sequence 00 --dataset /path/to/kitti/dataset/ 135 | ``` 136 | 137 | where: 138 | - `sequence` is the sequence to be accessed. 139 | - `dataset` is the path to the kitti dataset where the `sequences` directory is. 140 | 141 | Navigation: 142 | - `n` is next scan, 143 | - `b` is previous scan, 144 | - `esc` or `q` exits. 145 | 146 | In order to visualize your predictions instead, the `--predictions` option replaces 147 | visualization of the labels with the visualization of your predictions: 148 | 149 | ```sh 150 | $ ./visualize.py --sequence 00 --dataset /path/to/kitti/dataset/ --predictions /path/to/your/predictions 151 | ``` 152 | 153 | To directly compare two sets of data, use the `compare.py` script. It will open an interactive 154 | opengl visualization of the pointcloud labels. 155 | 156 | ```sh 157 | $ ./compare.py --sequence 00 --dataset_a /path/to/dataset_a/ --dataset_b /path/to/kitti/dataset_b/ 158 | ``` 159 | 160 | where: 161 | - `sequence` is the sequence to be accessed. 162 | - `dataset_a` is the path to a dataset in KITTI format where the `sequences` directory is. 163 | - `dataset_b` is the path to another dataset in KITTI format where the `sequences` directory is. 164 | 165 | Navigation: 166 | - `n` is next scan, 167 | - `b` is previous scan, 168 | - `esc` or `q` exits. 169 | 170 | #### Voxel Grids for Semantic Scene Completion 171 | 172 | To visualize the data, use the `visualize_voxels.py` script. It will open an interactive 173 | opengl visualization of the voxel grids and options to visualize the provided voxelizations 174 | of the LiDAR data. 175 | 176 | ```sh 177 | $ ./visualize_voxels.py --sequence 00 --dataset /path/to/kitti/dataset/ 178 | ``` 179 | 180 | where: 181 | - `sequence` is the sequence to be accessed. 182 | - `dataset` is the path to the kitti dataset where the `sequences` directory is. 183 | 184 | Navigation: 185 | - `n` is next scan, 186 | - `b` is previous scan, 187 | - `esc` or `q` exits. 188 | 189 | Note: Holding the forward/backward buttons triggers the playback mode. 190 | 191 | 192 | #### LiDAR-based Moving Object Segmentation ([LiDAR-MOS](https://github.com/PRBonn/LiDAR-MOS)) 193 | 194 | To visualize the data, use the `visualize_mos.py` script. It will open an interactive 195 | opengl visualization of the voxel grids and options to visualize the provided voxelizations 196 | of the LiDAR data. 197 | 198 | ```sh 199 | $ ./visualize_mos.py --sequence 00 --dataset /path/to/kitti/dataset/ 200 | ``` 201 | 202 | where: 203 | - `sequence` is the sequence to be accessed. 204 | - `dataset` is the path to the kitti dataset where the `sequences` directory is. 205 | 206 | Navigation: 207 | - `n` is next scan, 208 | - `b` is previous scan, 209 | - `esc` or `q` exits. 210 | 211 | Note: Holding the forward/backward buttons triggers the playback mode. 212 | 213 | 214 | #### Evaluation 215 | 216 | To evaluate the predictions of a method, use the [evaluate_semantics.py](./evaluate_semantics.py) to evaluate 217 | semantic segmentation, [evaluate_completion.py](./evaluate_completion.py) to evaluate the semantic scene completion and [evaluate_panoptic.py](./evaluate_panoptic.py) to evaluate panoptic segmentation. 218 | **Important:** The labels and the predictions need to be in the original 219 | label format, which means that if a method learns the cross-entropy mapped 220 | classes, they need to be passed through the `learning_map_inv` dictionary 221 | to be sent to the original dataset format. This is to prevent changes in the 222 | dataset interest classes from affecting intermediate outputs of approaches, 223 | since the original labels will stay the same. 224 | For semantic segmentation, we provide the `remap_semantic_labels.py` script to make this 225 | shift before the training, and once again before the evaluation, selecting which are the interest 226 | classes in the configuration file. 227 | The data needs to be either: 228 | 229 | - In a separate directory with this format: 230 | 231 | ``` 232 | /method_predictions/ 233 | └── sequences 234 | ├── 00 235 | │   └── predictions 236 | │ ├ 000000.label 237 | │ └ 000001.label 238 | ├── 01 239 | ├── 02 240 | . 241 | . 242 | . 243 | └── 21 244 | ``` 245 | 246 | And run: 247 | 248 | ```sh 249 | $ ./evaluate_semantics.py --dataset /path/to/kitti/dataset/ --predictions /path/to/method_predictions --split train/valid/test # depending of desired split to evaluate 250 | ``` 251 | 252 | or 253 | 254 | ```sh 255 | $ ./evaluate_completion.py --dataset /path/to/kitti/dataset/ --predictions /path/to/method_predictions --split train/valid/test # depending of desired split to evaluate 256 | ``` 257 | 258 | or 259 | 260 | ```sh 261 | $ ./evaluate_panoptic.py --dataset /path/to/kitti/dataset/ --predictions /path/to/method_predictions --split train/valid/test # depending of desired split to evaluate 262 | ``` 263 | 264 | or for moving object segmentation 265 | 266 | ```sh 267 | $ ./evaluate_mos.py --dataset /path/to/kitti/dataset/ --predictions /path/to/method_predictions --split train/valid/test # depending of desired split to evaluate 268 | ``` 269 | 270 | - In the same directory as the dataset 271 | 272 | ``` 273 | /kitti/dataset/ 274 | ├── poses 275 | └── sequences 276 | ├── 00 277 | │   ├── image_2 278 | │   ├── image_3 279 | │   ├── labels 280 | │ │ ├ 000000.label 281 | │ │ └ 000001.label 282 | │ ├── predictions 283 | │ │ ├ 000000.label 284 | │ │ └ 000001.label 285 | │   └── velodyne 286 | │ ├ 000000.bin 287 | │ └ 000001.bin 288 | ├── 01 289 | ├── 02 290 | . 291 | . 292 | . 293 | └── 21 294 | ``` 295 | 296 | And run (which sets the predictions directory as the same directory as the dataset): 297 | 298 | ```sh 299 | $ ./evaluate_semantics.py --dataset /path/to/kitti/dataset/ --split train/valid/test # depending of desired split to evaluate 300 | ``` 301 | 302 | If instead, the IoU vs distance is wanted, the evaluation is performed in the 303 | same way, but with the [evaluate_semantics_by_distance.py](./evaluate_semantics_by_distance.py) script. This will 304 | analyze the IoU for a set of 5 distance ranges: `{(0m:10m), [10m:20m), [20m:30m), [30m:40m), (40m:50m)}`. 305 | 306 | #### Validation 307 | 308 | To ensure that your zip file is valid, we provide a small validation script [validate_submission.py](./validate_submission.py) that checks for the correct folder structure and consistent number of labels for each scan. 309 | 310 | The submission folder expects to get an zip file containing the following folder structure (as the separate case above) 311 | 312 | ``` 313 | ├ description.txt (optional) 314 | sequences 315 | ├── 11 316 | │   └── predictions 317 | │ ├ 000000.label 318 | │ ├ 000001.label 319 | │ ├ ... 320 | ├── 12 321 | │   └── predictions 322 | │ ├ 000000.label 323 | │ ├ 000001.label 324 | │ ├ ... 325 | ├── 13 326 | . 327 | . 328 | . 329 | └── 21 330 | ``` 331 | 332 | In summary, you only have to provide the label files containing your predictions for every point of the scan and this is also checked by our validation script. 333 | 334 | Run: 335 | ```sh 336 | $ ./validate_submission.py --task {segmentation|completion|panoptic} /path/to/submission.zip /path/to/kitti/dataset 337 | ``` 338 | to check your `submission.zip`. 339 | 340 | ***Note:*** We don't check if the labels are valid, since invalid labels are simply ignored by the evaluation script. 341 | 342 | #### (New!) Adding Approach Information 343 | 344 | If you want to have more information on the leaderboard in the new updated Codalab competitions under the "Detailed Results", you have to provide an additional `description.txt` file to the submission archive containing information (here just an example): 345 | 346 | ``` 347 | name: Auto-MOS 348 | pdf url: https://arxiv.org/pdf/2201.04501.pdf 349 | code url: https://github.com/PRBonn/auto-mos 350 | ``` 351 | 352 | where `name` corresponds to the name of the method, `pdf url` is a link to the paper pdf url (or empty), and `code url` is a url that directs to the code (or empty). If the information is not available, we will use `Anonymous` for the name, and `n/a` for the urls. 353 | 354 | 355 | 356 | #### Statistics 357 | 358 | - [content.py](content.py) allows to evaluate the class content of the training 359 | set, in order to weigh the loss for training, handling imbalanced data. 360 | - [count.py](count.py) returns the scan count for each sequence in the data. 361 | 362 | #### Generation 363 | 364 | - [generate_sequential.py](generate_sequential.py) generates a sequence of scans using the manually looped closed poses used in our labeling tool, and stores them as individual point clouds. If, for example, we want to generate a dataset containing, for each point cloud, the aggregation of itself with the previous 4 scans, then: 365 | 366 | ```sh 367 | $ ./generate_sequential.py --dataset /path/to/kitti/dataset/ --sequence_length 5 --output /path/to/put/new/dataset 368 | ``` 369 | 370 | - [remap_semantic_labels.py](remap_semantic_labels.py) allows to remap the labels 371 | to and from the cross-entropy format, so that the labels can be used for training, 372 | and the predictions can be used for evaluation. This file uses the `learning_map` and 373 | `learning_map_inv` dictionaries from the config file to map the labels and predictions. 374 | 375 | ## Docker for API 376 | 377 | If not installing the requirements is preferred, then a [docker](https://docs.docker.com/install/linux/docker-ce/ubuntu/) container is 378 | provided to run the scripts. 379 | 380 | To build and run the container in an interactive session, which allows to run 381 | X11 apps (and GL), and copies this repo to the working directory, use 382 | 383 | ``` 384 | $ ./docker.sh /path/to/dataset 385 | ``` 386 | 387 | Where `/path/to/dataset` is the location of your semantic kitti dataset, and 388 | will be available inside the image in `~/data` or `/home/developer/data` 389 | inside the container for further usage with the api. This is done by creating 390 | a shared volume, so it can be any directory containing data that is to be used 391 | by the API scripts. 392 | 393 | ## Citation: 394 | 395 | If you use this dataset and/or this API in your work, please cite its [paper](https://arxiv.org/abs/1904.01416) 396 | 397 | ``` 398 | @inproceedings{behley2019iccv, 399 | author = {J. Behley and M. Garbade and A. Milioto and J. Quenzel and S. Behnke and C. Stachniss and J. Gall}, 400 | title = {{SemanticKITTI: A Dataset for Semantic Scene Understanding of LiDAR Sequences}}, 401 | booktitle = {Proc. of the IEEE/CVF International Conf.~on Computer Vision (ICCV)}, 402 | year = {2019} 403 | } 404 | ``` 405 | 406 | And the paper for the [original KITTI dataset](http://www.cvlibs.net/datasets/kitti/eval_odometry.php): 407 | 408 | ``` 409 | @inproceedings{geiger2012cvpr, 410 | author = {A. Geiger and P. Lenz and R. Urtasun}, 411 | title = {{Are we ready for Autonomous Driving? The KITTI Vision Benchmark Suite}}, 412 | booktitle = {Proc.~of the IEEE Conf.~on Computer Vision and Pattern Recognition (CVPR)}, 413 | pages = {3354--3361}, 414 | year = {2012}} 415 | ``` 416 | --------------------------------------------------------------------------------