├── JSENet_code ├── cpp_wrappers │ ├── compile_wrappers.sh │ ├── cpp_subsampling │ │ ├── grid_subsampling │ │ │ ├── grid_subsampling.cpp │ │ │ └── grid_subsampling.h │ │ ├── setup.py │ │ └── wrapper.cpp │ └── cpp_utils │ │ ├── cloud │ │ ├── cloud.cpp │ │ └── cloud.h │ │ └── nanoflann │ │ └── nanoflann.hpp ├── dataset_modules │ ├── S3DIS.py │ ├── Scannet.py │ └── common.py ├── kernels │ ├── convolution_ops.py │ ├── dispositions │ │ └── k_015_center.ply │ └── kernel_points.py ├── models │ ├── JSENet.py │ └── network_blocks.py ├── test_model.py ├── tf_custom_ops │ ├── compile_op.sh │ ├── cpp_utils │ │ ├── cloud │ │ │ ├── cloud.cpp │ │ │ └── cloud.h │ │ └── nanoflann │ │ │ └── nanoflann.hpp │ ├── tf_neighbors │ │ ├── neighbors │ │ │ ├── neighbors.cpp │ │ │ └── neighbors.h │ │ ├── tf_batch_neighbors.cpp │ │ └── tf_neighbors.cpp │ └── tf_subsampling │ │ ├── grid_subsampling │ │ ├── grid_subsampling.cpp │ │ └── grid_subsampling.h │ │ ├── tf_batch_subsampling.cpp │ │ └── tf_subsampling.cpp ├── training_S3DIS.py ├── training_Scannet.py └── utils │ ├── config.py │ ├── mesh.py │ ├── metrics.py │ ├── ply.py │ ├── tester.py │ └── trainer.py ├── LICENSE └── README.md /JSENet_code/cpp_wrappers/compile_wrappers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Compile cpp subsampling 4 | cd cpp_subsampling 5 | python3 setup.py build_ext --inplace 6 | cd .. 7 | 8 | -------------------------------------------------------------------------------- /JSENet_code/cpp_wrappers/cpp_subsampling/grid_subsampling/grid_subsampling.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "grid_subsampling.h" 3 | 4 | 5 | void grid_subsampling(vector& original_points, 6 | vector& subsampled_points, 7 | vector& original_features, 8 | vector& subsampled_features, 9 | vector& original_classes, 10 | vector& subsampled_classes, 11 | float sampleDl, 12 | int verbose) { 13 | 14 | // Initiate variables 15 | // ****************** 16 | 17 | // Number of points in the cloud 18 | size_t N = original_points.size(); 19 | 20 | // Dimension of the features 21 | size_t fdim = original_features.size() / N; 22 | size_t ldim = original_classes.size() / N; 23 | 24 | // Limits of the cloud 25 | PointXYZ minCorner = min_point(original_points); 26 | PointXYZ maxCorner = max_point(original_points); 27 | PointXYZ originCorner = floor(minCorner * (1/sampleDl)) * sampleDl; 28 | 29 | // Dimensions of the grid 30 | size_t sampleNX = (size_t)floor((maxCorner.x - originCorner.x) / sampleDl) + 1; 31 | size_t sampleNY = (size_t)floor((maxCorner.y - originCorner.y) / sampleDl) + 1; 32 | //size_t sampleNZ = (size_t)floor((maxCorner.z - originCorner.z) / sampleDl) + 1; 33 | 34 | // Check if features and classes need to be processed 35 | bool use_feature = original_features.size() > 0; 36 | bool use_classes = original_classes.size() > 0; 37 | 38 | 39 | // Create the sampled map 40 | // ********************** 41 | 42 | // Verbose parameters 43 | int i = 0; 44 | int nDisp = N / 100; 45 | 46 | // Initiate variables 47 | size_t iX, iY, iZ, mapIdx; 48 | unordered_map data; 49 | 50 | for (auto& p : original_points) 51 | { 52 | // Position of point in sample map 53 | iX = (size_t)floor((p.x - originCorner.x) / sampleDl); 54 | iY = (size_t)floor((p.y - originCorner.y) / sampleDl); 55 | iZ = (size_t)floor((p.z - originCorner.z) / sampleDl); 56 | mapIdx = iX + sampleNX*iY + sampleNX*sampleNY*iZ; 57 | 58 | // If not already created, create key 59 | if (data.count(mapIdx) < 1) 60 | data.emplace(mapIdx, SampledData(fdim, ldim)); 61 | 62 | // Fill the sample map 63 | if (use_feature && use_classes) 64 | data[mapIdx].update_all(p, original_features.begin() + i * fdim, original_classes.begin() + i * ldim); 65 | else if (use_feature) 66 | data[mapIdx].update_features(p, original_features.begin() + i * fdim); 67 | else if (use_classes) 68 | data[mapIdx].update_classes(p, original_classes.begin() + i * ldim); 69 | else 70 | data[mapIdx].update_points(p); 71 | 72 | // Display 73 | i++; 74 | if (verbose > 1 && i%nDisp == 0) 75 | std::cout << "\rSampled Map : " << std::setw(3) << i / nDisp << "%"; 76 | 77 | } 78 | 79 | // Divide for barycentre and transfer to a vector 80 | subsampled_points.reserve(data.size()); 81 | if (use_feature) 82 | subsampled_features.reserve(data.size() * fdim); 83 | if (use_classes) 84 | subsampled_classes.reserve(data.size() * ldim); 85 | for (auto& v : data) 86 | { 87 | subsampled_points.push_back(v.second.point * (1.0 / v.second.count)); 88 | if (use_feature) 89 | { 90 | float count = (float)v.second.count; 91 | transform(v.second.features.begin(), 92 | v.second.features.end(), 93 | v.second.features.begin(), 94 | [count](float f) { return f / count;}); 95 | subsampled_features.insert(subsampled_features.end(),v.second.features.begin(),v.second.features.end()); 96 | } 97 | if (use_classes) 98 | { 99 | for (int i = 0; i < ldim; i++) 100 | subsampled_classes.push_back(max_element(v.second.labels[i].begin(), v.second.labels[i].end())->first); 101 | } 102 | } 103 | 104 | return; 105 | } 106 | -------------------------------------------------------------------------------- /JSENet_code/cpp_wrappers/cpp_subsampling/grid_subsampling/grid_subsampling.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include "../../cpp_utils/cloud/cloud.h" 4 | 5 | #include 6 | #include 7 | 8 | using namespace std; 9 | 10 | class SampledData 11 | { 12 | public: 13 | 14 | // Elements 15 | // ******** 16 | 17 | int count; 18 | PointXYZ point; 19 | vector features; 20 | vector> labels; 21 | 22 | 23 | // Methods 24 | // ******* 25 | 26 | // Constructor 27 | SampledData() 28 | { 29 | count = 0; 30 | point = PointXYZ(); 31 | } 32 | 33 | SampledData(const size_t fdim, const size_t ldim) 34 | { 35 | count = 0; 36 | point = PointXYZ(); 37 | features = vector(fdim); 38 | labels = vector>(ldim); 39 | } 40 | 41 | // Method Update 42 | void update_all(const PointXYZ p, vector::iterator f_begin, vector::iterator l_begin) 43 | { 44 | count += 1; 45 | point += p; 46 | transform (features.begin(), features.end(), f_begin, features.begin(), plus()); 47 | int i = 0; 48 | for(vector::iterator it = l_begin; it != l_begin + labels.size(); ++it) 49 | { 50 | labels[i][*it] += 1; 51 | i++; 52 | } 53 | return; 54 | } 55 | void update_features(const PointXYZ p, vector::iterator f_begin) 56 | { 57 | count += 1; 58 | point += p; 59 | transform (features.begin(), features.end(), f_begin, features.begin(), plus()); 60 | return; 61 | } 62 | void update_classes(const PointXYZ p, vector::iterator l_begin) 63 | { 64 | count += 1; 65 | point += p; 66 | int i = 0; 67 | for(vector::iterator it = l_begin; it != l_begin + labels.size(); ++it) 68 | { 69 | labels[i][*it] += 1; 70 | i++; 71 | } 72 | return; 73 | } 74 | void update_points(const PointXYZ p) 75 | { 76 | count += 1; 77 | point += p; 78 | return; 79 | } 80 | }; 81 | 82 | 83 | 84 | void grid_subsampling(vector& original_points, 85 | vector& subsampled_points, 86 | vector& original_features, 87 | vector& subsampled_features, 88 | vector& original_classes, 89 | vector& subsampled_classes, 90 | float sampleDl, 91 | int verbose); 92 | 93 | -------------------------------------------------------------------------------- /JSENet_code/cpp_wrappers/cpp_subsampling/setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup, Extension 2 | import numpy.distutils.misc_util 3 | 4 | # Adding OpenCV to project 5 | # ************************ 6 | 7 | # Adding sources of the project 8 | # ***************************** 9 | 10 | m_name = "grid_subsampling" 11 | 12 | SOURCES = ["../cpp_utils/cloud/cloud.cpp", 13 | "grid_subsampling/grid_subsampling.cpp", 14 | "wrapper.cpp"] 15 | 16 | module = Extension(m_name, 17 | sources=SOURCES, 18 | extra_compile_args=['-std=c++11', 19 | '-D_GLIBCXX_USE_CXX11_ABI=0']) 20 | 21 | setup(ext_modules=[module], include_dirs=numpy.distutils.misc_util.get_numpy_include_dirs()) 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /JSENet_code/cpp_wrappers/cpp_subsampling/wrapper.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "grid_subsampling/grid_subsampling.h" 4 | #include 5 | 6 | 7 | 8 | // docstrings for our module 9 | // ************************* 10 | 11 | static char module_docstring[] = "This module provides an interface for the subsampling of a pointcloud"; 12 | 13 | static char compute_docstring[] = "function subsampling a pointcloud"; 14 | 15 | 16 | // Declare the functions 17 | // ********************* 18 | 19 | static PyObject *grid_subsampling_compute(PyObject *self, PyObject *args, PyObject *keywds); 20 | 21 | 22 | // Specify the members of the module 23 | // ********************************* 24 | 25 | static PyMethodDef module_methods[] = 26 | { 27 | { "compute", (PyCFunction)grid_subsampling_compute, METH_VARARGS | METH_KEYWORDS, compute_docstring }, 28 | {NULL, NULL, 0, NULL} 29 | }; 30 | 31 | 32 | // Initialize the module 33 | // ********************* 34 | 35 | static struct PyModuleDef moduledef = 36 | { 37 | PyModuleDef_HEAD_INIT, 38 | "grid_subsampling", // m_name 39 | module_docstring, // m_doc 40 | -1, // m_size 41 | module_methods, // m_methods 42 | NULL, // m_reload 43 | NULL, // m_traverse 44 | NULL, // m_clear 45 | NULL, // m_free 46 | }; 47 | 48 | PyMODINIT_FUNC PyInit_grid_subsampling(void) 49 | { 50 | import_array(); 51 | return PyModule_Create(&moduledef); 52 | } 53 | 54 | 55 | // Actual wrapper 56 | // ************** 57 | 58 | static PyObject *grid_subsampling_compute(PyObject *self, PyObject *args, PyObject *keywds) 59 | { 60 | 61 | // Manage inputs 62 | // ************* 63 | 64 | // Args containers 65 | PyObject *points_obj = NULL; 66 | PyObject *features_obj = NULL; 67 | PyObject *classes_obj = NULL; 68 | 69 | // Keywords containers 70 | static char *kwlist[] = {"points", "features", "classes", "sampleDl", "method", "verbose", NULL }; 71 | float sampleDl = 0.1; 72 | const char *method_buffer = "barycenters"; 73 | int verbose = 0; 74 | 75 | // Parse the input 76 | if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|$OOfsi", kwlist, &points_obj, &features_obj, &classes_obj, &sampleDl, &method_buffer, &verbose)) 77 | { 78 | PyErr_SetString(PyExc_RuntimeError, "Error parsing arguments"); 79 | return NULL; 80 | } 81 | 82 | // Get the method argument 83 | string method(method_buffer); 84 | 85 | // Interpret method 86 | if (method.compare("barycenters") && method.compare("voxelcenters")) 87 | { 88 | PyErr_SetString(PyExc_RuntimeError, "Error parsing method. Valid method names are \"barycenters\" and \"voxelcenters\" "); 89 | return NULL; 90 | } 91 | 92 | // Check if using features or classes 93 | bool use_feature = true, use_classes = true; 94 | if (features_obj == NULL) 95 | use_feature = false; 96 | if (classes_obj == NULL) 97 | use_classes = false; 98 | 99 | // Interpret the input objects as numpy arrays. 100 | PyObject *points_array = PyArray_FROM_OTF(points_obj, NPY_FLOAT, NPY_IN_ARRAY); 101 | PyObject *features_array = NULL; 102 | PyObject *classes_array = NULL; 103 | if (use_feature) 104 | features_array = PyArray_FROM_OTF(features_obj, NPY_FLOAT, NPY_IN_ARRAY); 105 | if (use_classes) 106 | classes_array = PyArray_FROM_OTF(classes_obj, NPY_INT, NPY_IN_ARRAY); 107 | 108 | // Verify data was load correctly. 109 | if (points_array == NULL) 110 | { 111 | Py_XDECREF(points_array); 112 | Py_XDECREF(classes_array); 113 | Py_XDECREF(features_array); 114 | PyErr_SetString(PyExc_RuntimeError, "Error converting input points to numpy arrays of type float32"); 115 | return NULL; 116 | } 117 | if (use_feature && features_array == NULL) 118 | { 119 | Py_XDECREF(points_array); 120 | Py_XDECREF(classes_array); 121 | Py_XDECREF(features_array); 122 | PyErr_SetString(PyExc_RuntimeError, "Error converting input features to numpy arrays of type float32"); 123 | return NULL; 124 | } 125 | if (use_classes && classes_array == NULL) 126 | { 127 | Py_XDECREF(points_array); 128 | Py_XDECREF(classes_array); 129 | Py_XDECREF(features_array); 130 | PyErr_SetString(PyExc_RuntimeError, "Error converting input classes to numpy arrays of type int32"); 131 | return NULL; 132 | } 133 | 134 | // Check that the input array respect the dims 135 | if ((int)PyArray_NDIM(points_array) != 2 || (int)PyArray_DIM(points_array, 1) != 3) 136 | { 137 | Py_XDECREF(points_array); 138 | Py_XDECREF(classes_array); 139 | Py_XDECREF(features_array); 140 | PyErr_SetString(PyExc_RuntimeError, "Wrong dimensions : points.shape is not (N, 3)"); 141 | return NULL; 142 | } 143 | if (use_feature && ((int)PyArray_NDIM(features_array) != 2)) 144 | { 145 | Py_XDECREF(points_array); 146 | Py_XDECREF(classes_array); 147 | Py_XDECREF(features_array); 148 | PyErr_SetString(PyExc_RuntimeError, "Wrong dimensions : features.shape is not (N, d)"); 149 | return NULL; 150 | } 151 | 152 | if (use_classes && (int)PyArray_NDIM(classes_array) > 2) 153 | { 154 | Py_XDECREF(points_array); 155 | Py_XDECREF(classes_array); 156 | Py_XDECREF(features_array); 157 | PyErr_SetString(PyExc_RuntimeError, "Wrong dimensions : classes.shape is not (N,) or (N, d)"); 158 | return NULL; 159 | } 160 | 161 | // Number of points 162 | int N = (int)PyArray_DIM(points_array, 0); 163 | 164 | // Dimension of the features 165 | int fdim = 0; 166 | if (use_feature) 167 | fdim = (int)PyArray_DIM(features_array, 1); 168 | 169 | //Dimension of labels 170 | int ldim = 1; 171 | if (use_classes && (int)PyArray_NDIM(classes_array) == 2) 172 | ldim = (int)PyArray_DIM(classes_array, 1); 173 | 174 | // Check that the input array respect the number of points 175 | if (use_feature && (int)PyArray_DIM(features_array, 0) != N) 176 | { 177 | Py_XDECREF(points_array); 178 | Py_XDECREF(classes_array); 179 | Py_XDECREF(features_array); 180 | PyErr_SetString(PyExc_RuntimeError, "Wrong dimensions : features.shape is not (N, d)"); 181 | return NULL; 182 | } 183 | if (use_classes && (int)PyArray_DIM(classes_array, 0) != N) 184 | { 185 | Py_XDECREF(points_array); 186 | Py_XDECREF(classes_array); 187 | Py_XDECREF(features_array); 188 | PyErr_SetString(PyExc_RuntimeError, "Wrong dimensions : classes.shape is not (N,) or (N, d)"); 189 | return NULL; 190 | } 191 | 192 | 193 | // Call the C++ function 194 | // ********************* 195 | 196 | // Create pyramid 197 | if (verbose > 0) 198 | cout << "Computing cloud pyramid with support points: " << endl; 199 | 200 | 201 | // Convert PyArray to Cloud C++ class 202 | vector original_points; 203 | vector original_features; 204 | vector original_classes; 205 | original_points = vector((PointXYZ*)PyArray_DATA(points_array), (PointXYZ*)PyArray_DATA(points_array) + N); 206 | if (use_feature) 207 | original_features = vector((float*)PyArray_DATA(features_array), (float*)PyArray_DATA(features_array) + N*fdim); 208 | if (use_classes) 209 | original_classes = vector((int*)PyArray_DATA(classes_array), (int*)PyArray_DATA(classes_array) + N*ldim); 210 | 211 | // Subsample 212 | vector subsampled_points; 213 | vector subsampled_features; 214 | vector subsampled_classes; 215 | grid_subsampling(original_points, 216 | subsampled_points, 217 | original_features, 218 | subsampled_features, 219 | original_classes, 220 | subsampled_classes, 221 | sampleDl, 222 | verbose); 223 | 224 | // Check result 225 | if (subsampled_points.size() < 1) 226 | { 227 | PyErr_SetString(PyExc_RuntimeError, "Error"); 228 | return NULL; 229 | } 230 | 231 | // Manage outputs 232 | // ************** 233 | 234 | // Dimension of input containers 235 | npy_intp* point_dims = new npy_intp[2]; 236 | point_dims[0] = subsampled_points.size(); 237 | point_dims[1] = 3; 238 | npy_intp* feature_dims = new npy_intp[2]; 239 | feature_dims[0] = subsampled_points.size(); 240 | feature_dims[1] = fdim; 241 | npy_intp* classes_dims = new npy_intp[2]; 242 | classes_dims[0] = subsampled_points.size(); 243 | classes_dims[1] = ldim; 244 | 245 | // Create output array 246 | PyObject *res_points_obj = PyArray_SimpleNew(2, point_dims, NPY_FLOAT); 247 | PyObject *res_features_obj = NULL; 248 | PyObject *res_classes_obj = NULL; 249 | PyObject *ret = NULL; 250 | 251 | // Fill output array with values 252 | size_t size_in_bytes = subsampled_points.size() * 3 * sizeof(float); 253 | memcpy(PyArray_DATA(res_points_obj), subsampled_points.data(), size_in_bytes); 254 | if (use_feature) 255 | { 256 | size_in_bytes = subsampled_points.size() * fdim * sizeof(float); 257 | res_features_obj = PyArray_SimpleNew(2, feature_dims, NPY_FLOAT); 258 | memcpy(PyArray_DATA(res_features_obj), subsampled_features.data(), size_in_bytes); 259 | } 260 | if (use_classes) 261 | { 262 | size_in_bytes = subsampled_points.size() * ldim * sizeof(int); 263 | res_classes_obj = PyArray_SimpleNew(2, classes_dims, NPY_INT); 264 | memcpy(PyArray_DATA(res_classes_obj), subsampled_classes.data(), size_in_bytes); 265 | } 266 | 267 | 268 | // Merge results 269 | if (use_feature && use_classes) 270 | ret = Py_BuildValue("NNN", res_points_obj, res_features_obj, res_classes_obj); 271 | else if (use_feature) 272 | ret = Py_BuildValue("NN", res_points_obj, res_features_obj); 273 | else if (use_classes) 274 | ret = Py_BuildValue("NN", res_points_obj, res_classes_obj); 275 | else 276 | ret = Py_BuildValue("N", res_points_obj); 277 | 278 | // Clean up 279 | // ******** 280 | 281 | Py_DECREF(points_array); 282 | Py_XDECREF(features_array); 283 | Py_XDECREF(classes_array); 284 | 285 | return ret; 286 | } -------------------------------------------------------------------------------- /JSENet_code/cpp_wrappers/cpp_utils/cloud/cloud.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // 3 | // 0==========================0 4 | // | Local feature test | 5 | // 0==========================0 6 | // 7 | // version 1.0 : 8 | // > 9 | // 10 | //--------------------------------------------------- 11 | // 12 | // Cloud source : 13 | // Define usefull Functions/Methods 14 | // 15 | //---------------------------------------------------- 16 | // 17 | // Hugues THOMAS - 10/02/2017 18 | // 19 | 20 | 21 | #include "cloud.h" 22 | 23 | 24 | // Getters 25 | // ******* 26 | 27 | PointXYZ max_point(std::vector points) 28 | { 29 | // Initiate limits 30 | PointXYZ maxP(points[0]); 31 | 32 | // Loop over all points 33 | for (auto p : points) 34 | { 35 | if (p.x > maxP.x) 36 | maxP.x = p.x; 37 | 38 | if (p.y > maxP.y) 39 | maxP.y = p.y; 40 | 41 | if (p.z > maxP.z) 42 | maxP.z = p.z; 43 | } 44 | 45 | return maxP; 46 | } 47 | 48 | PointXYZ min_point(std::vector points) 49 | { 50 | // Initiate limits 51 | PointXYZ minP(points[0]); 52 | 53 | // Loop over all points 54 | for (auto p : points) 55 | { 56 | if (p.x < minP.x) 57 | minP.x = p.x; 58 | 59 | if (p.y < minP.y) 60 | minP.y = p.y; 61 | 62 | if (p.z < minP.z) 63 | minP.z = p.z; 64 | } 65 | 66 | return minP; 67 | } -------------------------------------------------------------------------------- /JSENet_code/cpp_wrappers/cpp_utils/cloud/cloud.h: -------------------------------------------------------------------------------- 1 | // 2 | // 3 | // 0==========================0 4 | // | Local feature test | 5 | // 0==========================0 6 | // 7 | // version 1.0 : 8 | // > 9 | // 10 | //--------------------------------------------------- 11 | // 12 | // Cloud header 13 | // 14 | //---------------------------------------------------- 15 | // 16 | // Hugues THOMAS - 10/02/2017 17 | // 18 | 19 | 20 | # pragma once 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | #include 32 | 33 | 34 | 35 | 36 | // Point class 37 | // *********** 38 | 39 | 40 | class PointXYZ 41 | { 42 | public: 43 | 44 | // Elements 45 | // ******** 46 | 47 | float x, y, z; 48 | 49 | 50 | // Methods 51 | // ******* 52 | 53 | // Constructor 54 | PointXYZ() { x = 0; y = 0; z = 0; } 55 | PointXYZ(float x0, float y0, float z0) { x = x0; y = y0; z = z0; } 56 | 57 | // array type accessor 58 | float operator [] (int i) const 59 | { 60 | if (i == 0) return x; 61 | else if (i == 1) return y; 62 | else return z; 63 | } 64 | 65 | // opperations 66 | float dot(const PointXYZ P) const 67 | { 68 | return x * P.x + y * P.y + z * P.z; 69 | } 70 | 71 | float sq_norm() 72 | { 73 | return x*x + y*y + z*z; 74 | } 75 | 76 | PointXYZ cross(const PointXYZ P) const 77 | { 78 | return PointXYZ(y*P.z - z*P.y, z*P.x - x*P.z, x*P.y - y*P.x); 79 | } 80 | 81 | PointXYZ& operator+=(const PointXYZ& P) 82 | { 83 | x += P.x; 84 | y += P.y; 85 | z += P.z; 86 | return *this; 87 | } 88 | 89 | PointXYZ& operator-=(const PointXYZ& P) 90 | { 91 | x -= P.x; 92 | y -= P.y; 93 | z -= P.z; 94 | return *this; 95 | } 96 | 97 | PointXYZ& operator*=(const float& a) 98 | { 99 | x *= a; 100 | y *= a; 101 | z *= a; 102 | return *this; 103 | } 104 | }; 105 | 106 | 107 | // Point Opperations 108 | // ***************** 109 | 110 | inline PointXYZ operator + (const PointXYZ A, const PointXYZ B) 111 | { 112 | return PointXYZ(A.x + B.x, A.y + B.y, A.z + B.z); 113 | } 114 | 115 | inline PointXYZ operator - (const PointXYZ A, const PointXYZ B) 116 | { 117 | return PointXYZ(A.x - B.x, A.y - B.y, A.z - B.z); 118 | } 119 | 120 | inline PointXYZ operator * (const PointXYZ P, const float a) 121 | { 122 | return PointXYZ(P.x * a, P.y * a, P.z * a); 123 | } 124 | 125 | inline PointXYZ operator * (const float a, const PointXYZ P) 126 | { 127 | return PointXYZ(P.x * a, P.y * a, P.z * a); 128 | } 129 | 130 | inline std::ostream& operator << (std::ostream& os, const PointXYZ P) 131 | { 132 | return os << "[" << P.x << ", " << P.y << ", " << P.z << "]"; 133 | } 134 | 135 | inline bool operator == (const PointXYZ A, const PointXYZ B) 136 | { 137 | return A.x == B.x && A.y == B.y && A.z == B.z; 138 | } 139 | 140 | inline PointXYZ floor(const PointXYZ P) 141 | { 142 | return PointXYZ(floor(P.x), floor(P.y), floor(P.z)); 143 | } 144 | 145 | 146 | PointXYZ max_point(std::vector points); 147 | PointXYZ min_point(std::vector points); 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /JSENet_code/kernels/convolution_ops.py: -------------------------------------------------------------------------------- 1 | # 2 | # 3 | # 0=================================0 4 | # | Kernel Point Convolutions | 5 | # 0=================================0 6 | # 7 | # 8 | # ---------------------------------------------------------------------------------------------------------------------- 9 | # 10 | # Functions defining KPConv as tensorflow ops 11 | # 12 | # ---------------------------------------------------------------------------------------------------------------------- 13 | # 14 | # Hugues THOMAS - 11/06/2018 15 | # 16 | 17 | 18 | # ------------------------------------------------------------------------------------------ 19 | # 20 | # Imports and global variables 21 | # \**********************************/ 22 | # 23 | 24 | 25 | import numpy as np 26 | import tensorflow as tf 27 | import sys 28 | 29 | from kernels.kernel_points import load_kernels as create_kernel_points 30 | 31 | # ------------------------------------------------------------------------------------------ 32 | # 33 | # Utility function 34 | # \**********************/ 35 | # 36 | 37 | def broadcast_matmul(A, B): 38 | """ 39 | Compute A @ B, broadcasting over the first `N-2` ranks 40 | :param A: first matrix [..., d1, d2] 41 | :param B: second matrix [..., d2, d3] 42 | :return: result [..., d1, d3] 43 | """ 44 | """""" 45 | with tf.variable_scope("broadcast_matmul"): 46 | return tf.reduce_sum(A[..., tf.newaxis] * B[..., tf.newaxis, :, :], axis=-2) 47 | 48 | def radius_gaussian(sq_r, sig, eps=1e-9): 49 | """ 50 | Compute a radius gaussian (gaussian of distance) 51 | :param sq_r: input radiuses [dn, ..., d1, d0] 52 | :param sig: extents of gaussians [d1, d0] or [d0] or float 53 | :return: gaussian of sq_r [dn, ..., d1, d0] 54 | """ 55 | return tf.exp(-sq_r / (2 * tf.square(sig) + eps)) 56 | 57 | def general_gaussian(xyz, L): 58 | """ 59 | Compute a general gaussian deformable in every direction 60 | !!! params has to respect b^2 < ac or the gaussian is not defined !!! (Always use Cholesky decomposition) 61 | :param xyz: input radiuses [dn, ..., d1, d0, 3] 62 | :param L: Gaussian parameters in the forme of Cholesky decomposition [d1, d0, 3, 3] or [d0, 3, 3] or [3, 3] 63 | :return: gaussian of sq_xyz [dn, ..., d1, d0] 64 | """ 65 | 66 | if int(xyz.shape[-1]) != 3: 67 | raise ValueError('general_gaussian only defined for dimension 3') 68 | 69 | # Create symmetric definite-positive matrices 70 | if len(L.shape) == 3: 71 | A = tf.matmul(L, tf.transpose(L, [0, 2, 1])) 72 | elif len(L.shape) == 4: 73 | A = tf.matmul(L, tf.transpose(L, [0, 1, 3, 2])) 74 | else: 75 | raise ValueError('Matrix L in general gaussian have a wrong number of dimension') 76 | 77 | # Multiply by xyz from both sides 78 | quad = broadcast_matmul(tf.expand_dims(xyz, -2), tf.expand_dims(tf.expand_dims(A, 0), 0)) 79 | quad = broadcast_matmul(quad, tf.expand_dims(xyz, -1)) 80 | 81 | return tf.exp(-tf.squeeze(quad)) 82 | 83 | 84 | # ------------------------------------------------------------------------------------------ 85 | # 86 | # Convolutions definitions 87 | # \******************************/ 88 | # 89 | 90 | def unary_convolution(features, 91 | K_values): 92 | """ 93 | Simple unary convolution in tensorflow. Equivalent to matrix multiplication (space projection) for each features 94 | :param features: float32[n_points, in_fdim] - input features 95 | :param K_values: float32[in_fdim, out_fdim] - weights of the kernel 96 | :return: output_features float32[n_points, out_fdim] 97 | """ 98 | 99 | return tf.matmul(features, K_values) 100 | 101 | 102 | def KPConv(query_points, 103 | support_points, 104 | neighbors_indices, 105 | features, 106 | K_values, 107 | fixed='center', 108 | KP_extent=1.0, 109 | KP_influence='linear', 110 | aggregation_mode='sum'): 111 | """ 112 | This function initiates the kernel point disposition before building KPConv graph ops 113 | 114 | :param query_points: float32[n_points, dim] - input query points (center of neighborhoods) 115 | :param support_points: float32[n0_points, dim] - input support points (from which neighbors are taken) 116 | :param neighbors_indices: int32[n_points, n_neighbors] - indices of neighbors of each point 117 | :param features: float32[n_points, in_fdim] - input features 118 | :param K_values: float32[n_kpoints, in_fdim, out_fdim] - weights of the kernel 119 | :param fixed: string in ('none', 'center' or 'verticals') - fix position of certain kernel points 120 | :param KP_extent: float32 - influence radius of each kernel point 121 | :param KP_influence: string in ('constant', 'linear', 'gaussian') - influence function of the kernel points 122 | :param aggregation_mode: string in ('closest', 'sum') - whether to sum influences, or only keep the closest 123 | 124 | :return: output_features float32[n_points, out_fdim] 125 | """ 126 | 127 | # Initial kernel extent for this layer 128 | K_radius = 1.5 * KP_extent 129 | 130 | # Number of kernel points 131 | num_kpoints = int(K_values.shape[0]) 132 | 133 | # Check point dimension (currently only 3D is supported) 134 | points_dim = int(query_points.shape[1]) 135 | 136 | # Create one kernel disposition (as numpy array). Choose the KP distance to center thanks to the KP extent 137 | K_points_numpy = create_kernel_points(K_radius, 138 | num_kpoints, 139 | num_kernels=1, 140 | dimension=points_dim, 141 | fixed=fixed) 142 | K_points_numpy = K_points_numpy.reshape((num_kpoints, points_dim)) 143 | 144 | # Create the tensorflow variable 145 | K_points = tf.Variable(K_points_numpy.astype(np.float32), 146 | name='kernel_points', 147 | trainable=False, 148 | dtype=tf.float32) 149 | 150 | return KPConv_ops(query_points, 151 | support_points, 152 | neighbors_indices, 153 | features, 154 | K_points, 155 | K_values, 156 | KP_extent, 157 | KP_influence, 158 | aggregation_mode) 159 | 160 | 161 | def KPConv_ops(query_points, 162 | support_points, 163 | neighbors_indices, 164 | features, 165 | K_points, 166 | K_values, 167 | KP_extent, 168 | KP_influence, 169 | aggregation_mode): 170 | """ 171 | This function creates a graph of operations to define Kernel Point Convolution in tensorflow. See KPConv function 172 | above for a description of each parameter 173 | 174 | :param query_points: [n_points, dim] 175 | :param support_points: [n0_points, dim] 176 | :param neighbors_indices: [n_points, n_neighbors] 177 | :param features: [n_points, in_fdim] 178 | :param K_points: [n_kpoints, dim] 179 | :param K_values: [n_kpoints, in_fdim, out_fdim] 180 | :param KP_extent: float32 181 | :param KP_influence: string 182 | :param aggregation_mode: string 183 | :return: [n_points, out_fdim] 184 | """ 185 | 186 | # Get variables 187 | n_kp = int(K_points.shape[0]) 188 | 189 | # Add a fake point in the last row for shadow neighbors 190 | shadow_point = tf.ones_like(support_points[:1, :]) * 1e6 191 | support_points = tf.concat([support_points, shadow_point], axis=0) 192 | 193 | # Get neighbor points [n_points, n_neighbors, dim] 194 | neighbors = tf.gather(support_points, neighbors_indices, axis=0) 195 | 196 | # Center every neighborhood 197 | neighbors = neighbors - tf.expand_dims(query_points, 1) 198 | 199 | # Get all difference matrices [n_points, n_neighbors, n_kpoints, dim] 200 | neighbors = tf.expand_dims(neighbors, 2) 201 | neighbors = tf.tile(neighbors, [1, 1, n_kp, 1]) 202 | differences = neighbors - K_points 203 | 204 | # Get the square distances [n_points, n_neighbors, n_kpoints] 205 | sq_distances = tf.reduce_sum(tf.square(differences), axis=3) 206 | 207 | # Get Kernel point influences [n_points, n_kpoints, n_neighbors] 208 | if KP_influence == 'constant': 209 | # Every point get an influence of 1. 210 | all_weights = tf.ones_like(sq_distances) 211 | all_weights = tf.transpose(all_weights, [0, 2, 1]) 212 | 213 | elif KP_influence == 'linear': 214 | # Influence decrease linearly with the distance, and get to zero when d = KP_extent. 215 | all_weights = tf.maximum(1 - tf.sqrt(sq_distances) / KP_extent, 0.0) 216 | all_weights = tf.transpose(all_weights, [0, 2, 1]) 217 | 218 | elif KP_influence == 'gaussian': 219 | # Influence in gaussian of the distance. 220 | sigma = KP_extent * 0.3 221 | all_weights = radius_gaussian(sq_distances, sigma) 222 | all_weights = tf.transpose(all_weights, [0, 2, 1]) 223 | else: 224 | raise ValueError('Unknown influence function type (config.KP_influence)') 225 | 226 | # In case of closest mode, only the closest KP can influence each point 227 | if aggregation_mode == 'closest': 228 | neighbors_1nn = tf.argmin(sq_distances, axis=2, output_type=tf.int32) 229 | all_weights *= tf.one_hot(neighbors_1nn, n_kp, axis=1, dtype=tf.float32) 230 | 231 | elif aggregation_mode != 'sum': 232 | raise ValueError("Unknown convolution mode. Should be 'closest' or 'sum'") 233 | 234 | features = tf.concat([features, tf.zeros_like(features[:1, :])], axis=0) 235 | 236 | # Get the features of each neighborhood [n_points, n_neighbors, in_fdim] 237 | neighborhood_features = tf.gather(features, neighbors_indices, axis=0) 238 | 239 | # Apply distance weights [n_points, n_kpoints, in_fdim] 240 | weighted_features = tf.matmul(all_weights, neighborhood_features) 241 | 242 | # Apply network weights [n_kpoints, n_points, out_fdim] 243 | weighted_features = tf.transpose(weighted_features, [1, 0, 2]) 244 | kernel_outputs = tf.matmul(weighted_features, K_values) 245 | 246 | # Convolution sum to get [n_points, out_fdim] 247 | output_features = tf.reduce_sum(kernel_outputs, axis=0) 248 | 249 | return output_features 250 | 251 | 252 | def KPConv_deformable(query_points, 253 | support_points, 254 | neighbors_indices, 255 | features, 256 | K_values, 257 | fixed='center', 258 | KP_extent=1.0, 259 | KP_influence='linear', 260 | aggregation_mode='sum', 261 | modulated=False): 262 | """ 263 | This function initiates the kernel point disposition before building deformable KPConv graph ops 264 | 265 | :param query_points: float32[n_points, dim] - input query points (center of neighborhoods) 266 | :param support_points: float32[n0_points, dim] - input support points (from which neighbors are taken) 267 | :param neighbors_indices: int32[n_points, n_neighbors] - indices of neighbors of each point 268 | :param features: float32[n_points, in_fdim] - input features 269 | :param K_values: float32[n_kpoints, in_fdim, out_fdim] - weights of the kernel 270 | :param fixed: string in ('none', 'center' or 'verticals') - fix position of certain kernel points 271 | :param KP_extent: float32 - influence radius of each kernel point 272 | :param KP_influence: string in ('constant', 'linear', 'gaussian') - influence function of the kernel points 273 | :param aggregation_mode: string in ('closest', 'sum') - behavior of the convolution 274 | :param modulated: bool - If deformable conv should be modulated 275 | 276 | :return: output_features float32[n_points, out_fdim] 277 | """ 278 | 279 | ############ 280 | # Parameters 281 | ############ 282 | 283 | # Radius of the initial positions of the kernel points 284 | K_radius = 1.5 * KP_extent 285 | 286 | # Number of kernel points 287 | num_kpoints = int(K_values.shape[0]) 288 | 289 | # Check point dimension (currently only 3D is supported) 290 | points_dim = int(query_points.shape[1]) 291 | 292 | ################################# 293 | # Initiate kernel point positions 294 | ################################# 295 | 296 | # Create one kernel disposition (as numpy array). Choose the KP distance to center thanks to the KP extent 297 | K_points_numpy = create_kernel_points(K_radius, 298 | num_kpoints, 299 | num_kernels=1, 300 | dimension=points_dim, 301 | fixed=fixed) 302 | K_points_numpy = K_points_numpy.reshape((num_kpoints, points_dim)) 303 | 304 | # Create the tensorflow variable 305 | K_points = tf.Variable(K_points_numpy.astype(np.float32), 306 | name='kernel_points', 307 | trainable=False, 308 | dtype=tf.float32) 309 | 310 | ############################# 311 | # Standard KPConv for offsets 312 | ############################# 313 | 314 | # Create independant weight for the first convolution and a bias term as no batch normalization happen 315 | if modulated: 316 | offset_dim = (points_dim + 1) * num_kpoints 317 | else: 318 | offset_dim = points_dim * num_kpoints 319 | shape0 = K_values.shape.as_list() 320 | shape0[-1] = offset_dim 321 | K_values0 = tf.Variable(tf.zeros(shape0, dtype=tf.float32), name='offset_conv_weights') 322 | b0 = tf.Variable(tf.zeros(offset_dim, dtype=tf.float32), name='offset_conv_bias') 323 | 324 | # Get features from standard convolution 325 | features0 = KPConv_ops(query_points, 326 | support_points, 327 | neighbors_indices, 328 | features, 329 | K_points, 330 | K_values0, 331 | KP_extent, 332 | KP_influence, 333 | aggregation_mode) + b0 334 | 335 | if modulated: 336 | 337 | # Get offset (in normalized scale) from features 338 | offsets = features0[:, :points_dim * num_kpoints] 339 | offsets = tf.reshape(offsets, [-1, num_kpoints, points_dim]) 340 | 341 | # Get modulations 342 | modulations = 2 * tf.sigmoid(features0[:, points_dim * num_kpoints:]) 343 | 344 | else: 345 | 346 | # Get offset (in normalized scale) from features 347 | offsets = tf.reshape(features0, [-1, num_kpoints, points_dim]) 348 | 349 | # No modulations 350 | modulations = None 351 | 352 | # Rescale offset for this layer 353 | offsets *= KP_extent 354 | 355 | ############################### 356 | # Build deformable KPConv graph 357 | ############################### 358 | 359 | # Apply deformed convolution 360 | return KPConv_deform_ops(query_points, 361 | support_points, 362 | neighbors_indices, 363 | features, 364 | K_points, 365 | offsets, 366 | modulations, 367 | K_values, 368 | KP_extent, 369 | KP_influence, 370 | aggregation_mode) 371 | 372 | 373 | def KPConv_deform_ops(query_points, 374 | support_points, 375 | neighbors_indices, 376 | features, 377 | K_points, 378 | offsets, 379 | modulations, 380 | K_values, 381 | KP_extent, 382 | KP_influence, 383 | mode): 384 | """ 385 | This function creates a graph of operations to define Deformable Kernel Point Convolution in tensorflow. See 386 | KPConv_deformable function above for a description of each parameter 387 | 388 | :param query_points: [n_points, dim] 389 | :param support_points: [n0_points, dim] 390 | :param neighbors_indices: [n_points, n_neighbors] 391 | :param features: [n_points, in_fdim] 392 | :param K_points: [n_kpoints, dim] 393 | :param offsets: [n_points, n_kpoints, dim] 394 | :param modulations: [n_points, n_kpoints] or None 395 | :param K_values: [n_kpoints, in_fdim, out_fdim] 396 | :param KP_extent: float32 397 | :param KP_influence: string 398 | :param mode: string 399 | 400 | :return: [n_points, out_fdim] 401 | """ 402 | 403 | # Get variables 404 | n_kp = int(K_points.shape[0]) 405 | shadow_ind = tf.shape(support_points)[0] 406 | 407 | # Add a fake point in the last row for shadow neighbors 408 | shadow_point = tf.ones_like(support_points[:1, :]) * 1000 409 | support_points = tf.concat([support_points, shadow_point], axis=0) 410 | 411 | # Get neighbor points [n_points, n_neighbors, dim] 412 | neighbors = tf.gather(support_points, neighbors_indices, axis=0) 413 | 414 | # Center every neighborhood 415 | neighbors = neighbors - tf.expand_dims(query_points, 1) 416 | 417 | # Apply offsets to kernel points [n_points, n_kpoints, dim] 418 | deformed_K_points = tf.add(offsets, K_points, name='deformed_KP') 419 | 420 | # Get all difference matrices [n_points, n_neighbors, n_kpoints, dim] 421 | neighbors = tf.expand_dims(neighbors, 2) 422 | neighbors = tf.tile(neighbors, [1, 1, n_kp, 1]) 423 | differences = neighbors - tf.expand_dims(deformed_K_points, 1) 424 | 425 | # Get the square distances [n_points, n_neighbors, n_kpoints] 426 | sq_distances = tf.reduce_sum(tf.square(differences), axis=3, name='deformed_d2') 427 | 428 | # Boolean of the neighbors in range of a kernel point [n_points, n_neighbors] 429 | in_range = tf.cast(tf.reduce_any(tf.less(sq_distances, KP_extent**2), axis=2), tf.int32) 430 | 431 | # New value of max neighbors 432 | new_max_neighb = tf.reduce_max(tf.reduce_sum(in_range, axis=1)) 433 | 434 | # For each row of neighbors, indices of the ones that are in range [n_points, new_max_neighb] 435 | new_neighb_bool, new_neighb_inds = tf.math.top_k(in_range, k=new_max_neighb) 436 | 437 | # Gather new neighbor indices [n_points, new_max_neighb] 438 | new_neighbors_indices = tf.batch_gather(neighbors_indices, new_neighb_inds) 439 | 440 | # Gather new distances to KP [n_points, new_max_neighb, n_kpoints] 441 | new_sq_distances = tf.batch_gather(sq_distances, new_neighb_inds) 442 | 443 | # New shadow neighbors have to point to the last shadow point 444 | new_neighbors_indices *= new_neighb_bool 445 | new_neighbors_indices += (1 - new_neighb_bool) * shadow_ind 446 | 447 | # Get Kernel point influences [n_points, n_kpoints, n_neighbors] 448 | if KP_influence == 'constant': 449 | # Every point get an influence of 1. 450 | all_weights = tf.cast(new_sq_distances < KP_extent ** 2, tf.float32) 451 | all_weights = tf.transpose(all_weights, [0, 2, 1]) 452 | 453 | elif KP_influence == 'linear': 454 | # Influence decrease linearly with the distance, and get to zero when d = KP_extent. 455 | all_weights = tf.maximum(1 - tf.sqrt(new_sq_distances) / KP_extent, 0.0) 456 | all_weights = tf.transpose(all_weights, [0, 2, 1]) 457 | 458 | elif KP_influence == 'gaussian': 459 | # Influence in gaussian of the distance. 460 | sigma = KP_extent * 0.3 461 | all_weights = radius_gaussian(new_sq_distances, sigma) 462 | all_weights = tf.transpose(all_weights, [0, 2, 1]) 463 | else: 464 | raise ValueError('Unknown influence function type (config.KP_influence)') 465 | 466 | # In case of closest mode, only the closest KP can influence each point 467 | if mode == 'closest': 468 | neighbors_1nn = tf.argmin(new_sq_distances, axis=2, output_type=tf.int32) 469 | all_weights *= tf.one_hot(neighbors_1nn, n_kp, axis=1, dtype=tf.float32) 470 | 471 | elif mode != 'sum': 472 | raise ValueError("Unknown convolution mode. Should be 'closest' or 'sum'") 473 | 474 | features = tf.concat([features, tf.zeros_like(features[:1, :])], axis=0) 475 | 476 | # Get the features of each neighborhood [n_points, new_max_neighb, in_fdim] 477 | neighborhood_features = tf.gather(features, new_neighbors_indices, axis=0) 478 | 479 | # Apply distance weights [n_points, n_kpoints, in_fdim] 480 | weighted_features = tf.matmul(all_weights, neighborhood_features) 481 | 482 | # Apply modulations 483 | if modulations is not None: 484 | weighted_features *= tf.expand_dims(modulations, 2) 485 | 486 | # Apply network weights [n_kpoints, n_points, out_fdim] 487 | weighted_features = tf.transpose(weighted_features, [1, 0, 2]) 488 | kernel_outputs = tf.matmul(weighted_features, K_values) 489 | 490 | # Convolution sum [n_points, out_fdim] 491 | output_features = tf.reduce_sum(kernel_outputs, axis=0) 492 | 493 | return output_features 494 | 495 | 496 | # ------------------------------------------------------------------------------------------ 497 | # 498 | # DEV : Alternate deformable KPConv 499 | # \***************************************/ 500 | # 501 | 502 | 503 | def KPConv_deformable_v2(query_points, 504 | support_points, 505 | neighbors_indices, 506 | features, 507 | K_values, 508 | fixed='center', 509 | KP_extent=1.0, 510 | KP_influence='linear', 511 | aggregation_mode='sum', 512 | modulated=False): 513 | """ 514 | This alternate version uses a pointwise MLP instead of KPConv to get the offset. It has thus less parameters. 515 | It also fixes the center point to remain in the center in any case. This definition offers similar performances 516 | 517 | :param query_points: float32[n_points, dim] - input query points (center of neighborhoods) 518 | :param support_points: float32[n0_points, dim] - input support points (from which neighbors are taken) 519 | :param neighbors_indices: int32[n_points, n_neighbors] - indices of neighbors of each point 520 | :param features: float32[n_points, in_fdim] - input features 521 | :param K_values: float32[n_kpoints, in_fdim, out_fdim] - weights of the kernel 522 | :param fixed: string in ('none', 'center' or 'verticals') - fix position of certain kernel points 523 | :param KP_extent: float32 - influence radius of each kernel point 524 | :param KP_influence: string in ('constant', 'linear', 'gaussian') - influence function of the kernel points 525 | :param aggregation_mode: string in ('closest', 'sum') - behavior of the convolution 526 | :param modulated: bool - If deformable conv should be modulated 527 | 528 | :return: output_features float32[n_points, out_fdim] 529 | """ 530 | 531 | ############ 532 | # Parameters 533 | ############ 534 | 535 | # Check point dimension (currently only 3D is supported) 536 | points_dim = int(query_points.shape[1]) 537 | 538 | # Number of kernel points 539 | num_kpoints = int(K_values.shape[0]) 540 | 541 | ################# 542 | # MLP for offsets 543 | ################# 544 | 545 | # Create independant weight for the first convolution and a bias term as no batch normalization happen 546 | if modulated: 547 | offset_dim = (points_dim + 1) * (num_kpoints - 1) 548 | else: 549 | offset_dim = points_dim * (num_kpoints - 1) 550 | shape0 = K_values.shape.as_list() 551 | 552 | w0 = tf.Variable(tf.zeros([shape0[1], offset_dim], dtype=tf.float32), name='offset_mlp_weights') 553 | b0 = tf.Variable(tf.zeros([offset_dim], dtype=tf.float32), name='offset_mlp_bias') 554 | 555 | # Get features from mlp 556 | features0 = unary_convolution(features, w0) + b0 557 | 558 | if modulated: 559 | 560 | # Get offset (in normalized scale) from features 561 | offsets = features0[:, :points_dim * (num_kpoints - 1)] 562 | offsets = tf.reshape(offsets, [-1, (num_kpoints - 1), points_dim]) 563 | 564 | # Get modulations 565 | modulations = 2 * tf.sigmoid(features0[:, points_dim * (num_kpoints - 1):]) 566 | 567 | #  No offset for the first Kernel points 568 | offsets = tf.concat([tf.zeros_like(offsets[:, :1, :]), offsets], axis=1) 569 | modulations = tf.concat([tf.zeros_like(modulations[:, :1]), modulations], axis=1) 570 | 571 | else: 572 | 573 | # Get offset (in normalized scale) from features 574 | offsets = tf.reshape(features0, [-1, (num_kpoints - 1), points_dim]) 575 | 576 | # No offset for the first Kernel points 577 | offsets = tf.concat([tf.zeros_like(offsets[:, :1, :]), offsets], axis=1) 578 | 579 | # No modulations 580 | modulations = None 581 | 582 | # Rescale offset for this layer 583 | offsets *= KP_extent 584 | 585 | ################################# 586 | # Initiate kernel point positions 587 | ################################# 588 | 589 | # Radius of the initial positions of the kernel points 590 | K_radius = 1.5 * KP_extent 591 | 592 | # Create one kernel disposition (as numpy array). Choose the KP distance to center thanks to the KP extent 593 | K_points_numpy = create_kernel_points(K_radius, 594 | num_kpoints, 595 | num_kernels=1, 596 | dimension=points_dim, 597 | fixed=fixed) 598 | K_points_numpy = K_points_numpy.reshape((num_kpoints, points_dim)) 599 | 600 | # Create the tensorflow variable 601 | K_points = tf.Variable(K_points_numpy.astype(np.float32), 602 | name='kernel_points', 603 | trainable=False, 604 | dtype=tf.float32) 605 | 606 | ############################### 607 | # Build deformable KPConv graph 608 | ############################### 609 | 610 | # Apply deformed convolution 611 | return KPConv_deform_ops(query_points, 612 | support_points, 613 | neighbors_indices, 614 | features, 615 | K_points, 616 | offsets, 617 | modulations, 618 | K_values, 619 | KP_extent, 620 | KP_influence, 621 | aggregation_mode) 622 | 623 | 624 | 625 | 626 | 627 | -------------------------------------------------------------------------------- /JSENet_code/kernels/dispositions/k_015_center.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hzykent/JSENet/c16e9971624d08e6961237f21f35104fb29e8b1d/JSENet_code/kernels/dispositions/k_015_center.ply -------------------------------------------------------------------------------- /JSENet_code/kernels/kernel_points.py: -------------------------------------------------------------------------------- 1 | # 2 | # 3 | # 0=================================0 4 | # | Kernel Point Convolutions | 5 | # 0=================================0 6 | # 7 | # 8 | # ---------------------------------------------------------------------------------------------------------------------- 9 | # 10 | # Functions handling the disposition of kernel points. 11 | # 12 | # ---------------------------------------------------------------------------------------------------------------------- 13 | # 14 | # Hugues THOMAS - 11/06/2018 15 | # 16 | 17 | 18 | # ------------------------------------------------------------------------------------------ 19 | # 20 | # Imports and global variables 21 | # \**********************************/ 22 | # 23 | 24 | 25 | # Import numpy package and name it "np" 26 | import numpy as np 27 | import matplotlib.pyplot as plt 28 | from os import makedirs 29 | from os.path import join, exists 30 | 31 | from utils.ply import read_ply, write_ply 32 | 33 | 34 | # ------------------------------------------------------------------------------------------ 35 | # 36 | # Functions 37 | # \***************/ 38 | # 39 | # 40 | 41 | def kernel_point_optimization_debug(radius, num_points, num_kernels=1, dimension=3, fixed='center', ratio=1.0, verbose=0): 42 | """ 43 | Creation of kernel point via optimization of potentials. 44 | :param radius: Radius of the kernels 45 | :param num_points: points composing kernels 46 | :param num_kernels: number of wanted kernels 47 | :param dimension: dimension of the space 48 | :param fixed: fix position of certain kernel points ('none', 'center' or 'verticals') 49 | :param ratio: ratio of the radius where you want the kernels points to be placed 50 | :param verbose: display option 51 | :return: points [num_kernels, num_points, dimension] 52 | """ 53 | 54 | ####################### 55 | # Parameters definition 56 | ####################### 57 | 58 | # Radius used for optimization (points are rescaled afterwards) 59 | radius0 = 1 60 | diameter0 = 2 61 | 62 | # Factor multiplicating gradients for moving points (~learning rate) 63 | moving_factor = 1e-2 64 | continuous_moving_decay = 0.9995 65 | 66 | # Gradient threshold to stop optimization 67 | thresh = 1e-5 68 | 69 | # Gradient clipping value 70 | clip = 0.05 * radius0 71 | 72 | ####################### 73 | # Kernel initialization 74 | ####################### 75 | 76 | # Random kernel points 77 | kernel_points = np.random.rand(num_kernels * num_points - 1, dimension) * diameter0 - radius0 78 | while (kernel_points.shape[0] < num_kernels * num_points): 79 | new_points = np.random.rand(num_kernels * num_points - 1, dimension) * diameter0 - radius0 80 | kernel_points = np.vstack((kernel_points, new_points)) 81 | d2 = np.sum(np.power(kernel_points, 2), axis=1) 82 | kernel_points = kernel_points[d2 < 0.5 * radius0 * radius0, :] 83 | kernel_points = kernel_points[:num_kernels * num_points, :].reshape((num_kernels, num_points, -1)) 84 | 85 | # Optionnal fixing 86 | if fixed == 'center': 87 | kernel_points[:, 0, :] *= 0 88 | if fixed == 'verticals': 89 | kernel_points[:, :3, :] *= 0 90 | kernel_points[:, 1, -1] += 2 * radius0 / 3 91 | kernel_points[:, 2, -1] -= 2 * radius0 / 3 92 | 93 | ##################### 94 | # Kernel optimization 95 | ##################### 96 | 97 | # Initiate figure 98 | if verbose>1: 99 | fig = plt.figure() 100 | 101 | saved_gradient_norms = np.zeros((10000, num_kernels)) 102 | old_gradient_norms = np.zeros((num_kernels, num_points)) 103 | for iter in range(10000): 104 | 105 | # Compute gradients 106 | # ***************** 107 | 108 | # Derivative of the sum of potentials of all points 109 | A = np.expand_dims(kernel_points, axis=2) 110 | B = np.expand_dims(kernel_points, axis=1) 111 | interd2 = np.sum(np.power(A - B, 2), axis=-1) 112 | inter_grads = (A - B) / (np.power(np.expand_dims(interd2, -1), 3/2) + 1e-6) 113 | inter_grads = np.sum(inter_grads, axis=1) 114 | 115 | # Derivative of the radius potential 116 | circle_grads = 10*kernel_points 117 | 118 | # All gradients 119 | gradients = inter_grads + circle_grads 120 | 121 | if fixed == 'verticals': 122 | gradients[:, 1:3, :-1] = 0 123 | 124 | # Stop condition 125 | # ************** 126 | 127 | # Compute norm of gradients 128 | gradients_norms = np.sqrt(np.sum(np.power(gradients, 2), axis=-1)) 129 | saved_gradient_norms[iter, :] = np.max(gradients_norms, axis=1) 130 | 131 | # Stop if all moving points are gradients fixed (low gradients diff) 132 | 133 | if fixed == 'center' and np.max(np.abs(old_gradient_norms[:, 1:] - gradients_norms[:, 1:])) < thresh: 134 | break 135 | elif fixed == 'verticals' and np.max(np.abs(old_gradient_norms[:, 3:] - gradients_norms[:, 3:])) < thresh: 136 | break 137 | elif np.max(np.abs(old_gradient_norms - gradients_norms)) < thresh: 138 | break 139 | old_gradient_norms = gradients_norms 140 | 141 | # Move points 142 | # *********** 143 | 144 | # Clip gradient to get moving dists 145 | moving_dists = np.minimum(moving_factor * gradients_norms, clip) 146 | 147 | # Fix central point 148 | if fixed == 'center': 149 | moving_dists[:, 0] = 0 150 | if fixed == 'verticals': 151 | moving_dists[:, 0] = 0 152 | 153 | # Move points 154 | kernel_points -= np.expand_dims(moving_dists, -1) * gradients / np.expand_dims(gradients_norms + 1e-6, -1) 155 | 156 | if verbose: 157 | print('iter {:5d} / max grad = {:f}'.format(iter, np.max(gradients_norms[:, 3:]))) 158 | if verbose > 1: 159 | plt.clf() 160 | plt.plot(kernel_points[0, :, 0], kernel_points[0, :, 1], '.') 161 | circle = plt.Circle((0, 0), radius, color='r', fill=False) 162 | fig.axes[0].add_artist(circle) 163 | fig.axes[0].set_xlim((-radius*1.1, radius*1.1)) 164 | fig.axes[0].set_ylim((-radius*1.1, radius*1.1)) 165 | fig.axes[0].set_aspect('equal') 166 | plt.draw() 167 | plt.pause(0.001) 168 | plt.show(block=False) 169 | print(moving_factor) 170 | 171 | # moving factor decay 172 | moving_factor *= continuous_moving_decay 173 | 174 | # Rescale radius to fit the wanted ratio of radius 175 | r = np.sqrt(np.sum(np.power(kernel_points, 2), axis=-1)) 176 | kernel_points *= ratio / np.mean(r[:, 1:]) 177 | 178 | # Rescale kernels with real radius 179 | return kernel_points * radius, saved_gradient_norms 180 | 181 | 182 | def load_kernels(radius, num_kpoints, num_kernels, dimension, fixed): 183 | 184 | # Number of tries in the optimization process, to ensure we get the most stable disposition 185 | num_tries = 100 186 | 187 | # Kernel directory 188 | kernel_dir = 'kernels/dispositions' 189 | if not exists(kernel_dir): 190 | makedirs(kernel_dir) 191 | 192 | # Kernel_file 193 | if dimension == 3: 194 | kernel_file = join(kernel_dir, 'k_{:03d}_{:s}.ply'.format(num_kpoints, fixed)) 195 | elif dimension == 2: 196 | kernel_file = join(kernel_dir, 'k_{:03d}_{:s}_2D.ply'.format(num_kpoints, fixed)) 197 | else: 198 | raise ValueError('Unsupported dimpension of kernel : ' + str(dimension)) 199 | 200 | # Check if already done 201 | if not exists(kernel_file): 202 | 203 | # Create kernels 204 | kernel_points, grad_norms = kernel_point_optimization_debug(1.0, 205 | num_kpoints, 206 | num_kernels=num_tries, 207 | dimension=dimension, 208 | fixed=fixed, 209 | verbose=0) 210 | 211 | # Find best candidate 212 | best_k = np.argmin(grad_norms[-1, :]) 213 | 214 | # Save points 215 | original_kernel = kernel_points[best_k, :, :] 216 | write_ply(kernel_file, original_kernel, ['x', 'y', 'z']) 217 | 218 | else: 219 | data = read_ply(kernel_file) 220 | original_kernel = np.vstack((data['x'], data['y'], data['z'])).T 221 | 222 | # N.B. 2D kernels are not supported yet 223 | if dimension == 2: 224 | return original_kernel 225 | 226 | # Random rotations depending of the fixed points 227 | if fixed == 'verticals': 228 | 229 | # Create random rotations 230 | thetas = np.random.rand(num_kernels) * 2 * np.pi 231 | c, s = np.cos(thetas), np.sin(thetas) 232 | R = np.zeros((num_kernels, 3, 3), dtype=np.float32) 233 | R[:, 0, 0] = c 234 | R[:, 1, 1] = c 235 | R[:, 2, 2] = 1 236 | R[:, 0, 1] = s 237 | R[:, 1, 0] = -s 238 | 239 | # Scale kernels 240 | original_kernel = radius * np.expand_dims(original_kernel, 0) 241 | 242 | # Rotate kernels 243 | kernels = np.matmul(original_kernel, R) 244 | 245 | else: 246 | 247 | # Create random rotations 248 | u = np.ones((num_kernels, 3)) 249 | v = np.ones((num_kernels, 3)) 250 | wrongs = np.abs(np.sum(u * v, axis=1)) > 0.99 251 | while np.any(wrongs): 252 | new_u = np.random.rand(num_kernels, 3) * 2 - 1 253 | new_u = new_u / np.expand_dims(np.linalg.norm(new_u, axis=1) + 1e-9, -1) 254 | u[wrongs, :] = new_u[wrongs, :] 255 | new_v = np.random.rand(num_kernels, 3) * 2 - 1 256 | new_v = new_v / np.expand_dims(np.linalg.norm(new_v, axis=1) + 1e-9, -1) 257 | v[wrongs, :] = new_v[wrongs, :] 258 | wrongs = np.abs(np.sum(u * v, axis=1)) > 0.99 259 | 260 | # Make v perpendicular to u 261 | v -= np.expand_dims(np.sum(u * v, axis=1), -1) * u 262 | v = v / np.expand_dims(np.linalg.norm(v, axis=1) + 1e-9, -1) 263 | 264 | # Last rotation vector 265 | w = np.cross(u, v) 266 | R = np.stack((u, v, w), axis=-1) 267 | 268 | # Scale kernels 269 | original_kernel = radius * np.expand_dims(original_kernel, 0) 270 | 271 | # Rotate kernels 272 | kernels = np.matmul(original_kernel, R) 273 | 274 | # Add a small noise 275 | kernels = kernels 276 | kernels = kernels + np.random.normal(scale=radius*0.01, size=kernels.shape) 277 | 278 | return kernels 279 | 280 | 281 | 282 | 283 | -------------------------------------------------------------------------------- /JSENet_code/models/JSENet.py: -------------------------------------------------------------------------------- 1 | # Basic libs 2 | from os import makedirs 3 | from os.path import exists 4 | import time 5 | import tensorflow as tf 6 | import sys 7 | 8 | # Convolution functions 9 | from models.network_blocks import assemble_CNN_blocks, assemble_DCNN_blocks_edge, assemble_DCNN_blocks_region, segmentation_head_edge, segmentation_head_region 10 | from models.network_blocks import side_0_head, side_1_head, side_2_head, side_3_head, side_4_head 11 | from models.network_blocks import segmentation_loss_edge, segmentation_loss_region, bce_loss, segmentation_loss_efr 12 | from models.network_blocks import simple_block, nearest_upsample_block, unary_block, simple_upsample_block, feature_fusion, edge_generation 13 | 14 | 15 | 16 | 17 | # ---------------------------------------------------------------------------------------------------------------------- 18 | # 19 | # Model Class 20 | # \*****************/ 21 | # 22 | 23 | 24 | class JSENet: 25 | 26 | def __init__(self, flat_inputs, config): 27 | """ 28 | Initiate the model 29 | :param flat_inputs: List of input tensors (flatten) 30 | :param config: configuration class 31 | """ 32 | 33 | # Model parameters 34 | self.config = config 35 | 36 | # Path of the result folder 37 | if self.config.saving: 38 | if self.config.saving_path == None: 39 | self.saving_path = 'results/JSENet_' + self.config.dataset 40 | else: 41 | self.saving_path = self.config.saving_path 42 | if not exists(self.saving_path): 43 | makedirs(self.saving_path) 44 | 45 | 46 | ######## 47 | # Inputs 48 | ######## 49 | 50 | # Sort flatten inputs in a dictionary 51 | with tf.variable_scope('inputs'): 52 | self.inputs = dict() 53 | # flat_inputs[i] corresponding to specific data for a batch of point clouds 54 | # input point positions for a batch_size of clouds for all network layers 55 | self.inputs['points'] = flat_inputs[:config.num_layers] 56 | # corresponding neighbors for all network layers 57 | self.inputs['neighbors'] = flat_inputs[config.num_layers:2 * config.num_layers] 58 | self.inputs['pools'] = flat_inputs[2 * config.num_layers:3 * config.num_layers] 59 | self.inputs['upsamples'] = flat_inputs[3 * config.num_layers:4 * config.num_layers] 60 | ind = 4 * config.num_layers 61 | self.inputs['features'] = flat_inputs[ind] 62 | ind += 1 63 | self.inputs['batch_weights'] = flat_inputs[ind] 64 | ind += 1 65 | self.inputs['in_batches'] = flat_inputs[ind] 66 | ind += 1 67 | self.inputs['out_batches'] = flat_inputs[ind] 68 | ind += 1 69 | self.inputs['point_labels'] = flat_inputs[ind] 70 | ind += 1 71 | self.inputs['point_boundaries'] = flat_inputs[ind] 72 | ind += 1 73 | self.inputs['point_b_c_0'] = flat_inputs[ind] 74 | ind += 1 75 | self.inputs['point_b_c_1'] = flat_inputs[ind] 76 | ind += 1 77 | self.inputs['point_b_c_2'] = flat_inputs[ind] 78 | ind += 1 79 | self.inputs['convert_neighbors'] = flat_inputs[ind] 80 | ind += 1 81 | # zeor based but ignored classes not eliminated 82 | self.labels = self.inputs['point_labels'] 83 | self.boundaries = self.inputs['point_boundaries'] 84 | self.b_c_0 = self.inputs['point_b_c_0'] 85 | self.b_c_1 = self.inputs['point_b_c_1'] 86 | self.b_c_2 = self.inputs['point_b_c_2'] 87 | 88 | self.inputs['augment_scales'] = flat_inputs[ind] 89 | ind += 1 90 | self.inputs['augment_rotations'] = flat_inputs[ind] 91 | ind += 1 92 | self.inputs['point_inds'] = flat_inputs[ind] 93 | ind += 1 94 | self.inputs['cloud_inds'] = flat_inputs[ind] 95 | 96 | # Dropout placeholder 97 | self.dropout_prob = tf.placeholder(tf.float32, name='dropout_prob') 98 | 99 | 100 | ######## 101 | # Layers 102 | ######## 103 | 104 | # Create layers 105 | with tf.variable_scope('KernelPointNetwork'): 106 | extracted_features = assemble_CNN_blocks(self.inputs, 107 | self.config, 108 | self.dropout_prob) 109 | 110 | features_edge = assemble_DCNN_blocks_edge(extracted_features, 111 | self.inputs, 112 | self.config, 113 | self.dropout_prob) 114 | 115 | features_region = assemble_DCNN_blocks_region(extracted_features, 116 | self.inputs, 117 | self.config, 118 | self.dropout_prob) 119 | 120 | # feature maps of each CNN layer 121 | feature_64 = extracted_features[0] 122 | feature_128 = extracted_features[1] 123 | feature_256 = extracted_features[2] 124 | feature_512 = extracted_features[3] 125 | feature_1024 = extracted_features[4] 126 | 127 | training = self.dropout_prob < 0.99 128 | 129 | 130 | with tf.variable_scope('region_branch'): 131 | 132 | self.logits_region_coarse = segmentation_head_region(features_region, 133 | self.config, 134 | self.dropout_prob) 135 | 136 | 137 | with tf.variable_scope('edge_branch'): 138 | # side feature after 1x1 convolution 139 | side_0_feature_edge = unary_block(-1, -1, feature_64, -1, self.config.num_classes, self.config, training) 140 | side_1_feature_edge = unary_block(-1, -1, feature_128, -1, self.config.num_classes, self.config, training) 141 | side_2_feature_edge = unary_block(-1, -1, feature_256, -1, self.config.num_classes, self.config, training) 142 | side_3_feature_edge = unary_block(-1, -1, feature_512, -1, self.config.num_classes, self.config, training) 143 | side_4_feature_edge = unary_block(-1, -1, feature_1024, -1, self.config.num_classes, self.config, training) 144 | 145 | # upsample to original size 146 | r0 = self.config.first_subsampling_dl * self.config.density_parameter 147 | side_0_feature_edge = simple_block(0, self.inputs, side_0_feature_edge, r0, self.config.num_classes, self.config, training) 148 | r1 = r0 * 2 149 | side_1_feature_edge = simple_upsample_block(1, self.inputs, side_1_feature_edge, r1, self.config.num_classes, self.config, training) 150 | r2 = r1 * 2 151 | side_2_feature_edge = nearest_upsample_block(2, self.inputs, side_2_feature_edge, r2, self.config.num_classes, self.config, training) 152 | side_2_feature_edge = simple_upsample_block(1, self.inputs, side_2_feature_edge, r1, self.config.num_classes, self.config, training) 153 | r3 = r2 * 2 154 | side_3_feature_edge = nearest_upsample_block(3, self.inputs, side_3_feature_edge, r3, self.config.num_classes, self.config, training) 155 | side_3_feature_edge = nearest_upsample_block(2, self.inputs, side_3_feature_edge, r2, self.config.num_classes, self.config, training) 156 | side_3_feature_edge = simple_upsample_block(1, self.inputs, side_3_feature_edge, r1, self.config.num_classes, self.config, training) 157 | r4 = r3 * 2 158 | side_4_feature_edge = nearest_upsample_block(4, self.inputs, side_4_feature_edge, r4, self.config.num_classes, self.config, training) 159 | side_4_feature_edge = nearest_upsample_block(3, self.inputs, side_4_feature_edge, r3, self.config.num_classes, self.config, training) 160 | side_4_feature_edge = nearest_upsample_block(2, self.inputs, side_4_feature_edge, r2, self.config.num_classes, self.config, training) 161 | side_4_feature_edge = simple_upsample_block(1, self.inputs, side_4_feature_edge, r1, self.config.num_classes, self.config, training) 162 | 163 | # side_0_logits 164 | self.side_0_logits_edge = side_0_head(side_0_feature_edge, 165 | self.config, 166 | self.dropout_prob) 167 | 168 | # side_1_logits 169 | self.side_1_logits_edge = side_1_head(side_1_feature_edge, 170 | self.config, 171 | self.dropout_prob) 172 | 173 | # side_2_logits 174 | self.side_2_logits_edge = side_2_head(side_2_feature_edge, 175 | self.config, 176 | self.dropout_prob) 177 | 178 | # side_3_logits 179 | self.side_3_logits_edge = side_3_head(side_3_feature_edge, 180 | self.config, 181 | self.dropout_prob) 182 | 183 | # side_4_logits 184 | self.side_4_logits_edge = side_4_head(side_4_feature_edge, 185 | self.config, 186 | self.dropout_prob) 187 | 188 | # concatenation 189 | features_edge = tf.concat([features_edge, side_4_feature_edge, 190 | side_3_feature_edge, side_2_feature_edge, side_1_feature_edge, side_0_feature_edge], axis=1) 191 | 192 | self.logits_edge_coarse = segmentation_head_edge(features_edge, 193 | self.config, 194 | self.dropout_prob) 195 | 196 | 197 | with tf.variable_scope('refine_module'): 198 | 199 | with tf.variable_scope('refinement_region_s0'): 200 | 201 | # Feature fusion sub-module 202 | logits_refine_region_s0 = tf.concat((self.logits_region_coarse, self.logits_edge_coarse), axis=1) 203 | refine_r_feature_s0 = feature_fusion(self.inputs, logits_refine_region_s0, self.config, training) 204 | self.logits_region_s0 = segmentation_head_region(refine_r_feature_s0, self.config, self.dropout_prob) 205 | 206 | # Edge map generation sub-module 207 | self.edge_map_fr_s0 = edge_generation(self.inputs, self.logits_region_s0) 208 | 209 | 210 | with tf.variable_scope('refinement_edge_s0'): 211 | 212 | # Edge map generation sub-module 213 | self.edge_map_fr_coarse = edge_generation(self.inputs, self.logits_region_coarse) 214 | 215 | # Feature fusion sub-module 216 | logits_edge_coarse_sigmoid = tf.math.sigmoid(self.logits_edge_coarse) 217 | logits_refine_edge_s0 = tf.concat((logits_edge_coarse_sigmoid, self.edge_map_fr_coarse), axis=1) 218 | refine_e_feature_s0 = feature_fusion(self.inputs, logits_refine_edge_s0, self.config, training) 219 | adding_edge_s0 = segmentation_head_edge(refine_e_feature_s0, self.config, self.dropout_prob) 220 | self.logits_edge_s0 = self.logits_edge_coarse + adding_edge_s0 221 | 222 | 223 | with tf.variable_scope('refinement_region_s1'): 224 | 225 | # Feature fusion sub-module 226 | logits_refine_region_s1 = tf.concat((self.logits_region_s0, self.logits_edge_s0), axis=1) 227 | refine_r_feature_s1 = feature_fusion(self.inputs, logits_refine_region_s1, self.config, training) 228 | self.logits_region_s1 = segmentation_head_region(refine_r_feature_s1, self.config, self.dropout_prob) 229 | 230 | # Edge map generation sub-module 231 | self.edge_map_fr_s1 = edge_generation(self.inputs, self.logits_region_s1) 232 | 233 | 234 | with tf.variable_scope('refinement_edge_s1'): 235 | 236 | # Feature fusion sub-module 237 | logits_edge_s0_sigmoid = tf.math.sigmoid(self.logits_edge_s0) 238 | logits_refine_edge_s1 = tf.concat((logits_edge_s0_sigmoid, self.edge_map_fr_s0), axis=1) 239 | refine_e_feature_s1 = feature_fusion(self.inputs, logits_refine_edge_s1, self.config, training) 240 | adding_edge_s1 = segmentation_head_edge(refine_e_feature_s1, self.config, self.dropout_prob) 241 | self.logits_edge_s1 = self.logits_edge_s0 + adding_edge_s1 242 | 243 | 244 | ######## 245 | # Losses 246 | ######## 247 | 248 | with tf.variable_scope('loss'): 249 | if len(self.config.ignored_label_inds) > 0: 250 | # Boolean mask of points that should be ignored 251 | # 1. unclassified points 252 | # 2. unconsidered points 253 | 254 | # ignored_bool : 1 + 2 255 | ignored_bool = tf.zeros_like(self.labels, dtype=tf.bool) 256 | for ign_label in self.config.ignored_label_inds: 257 | ignored_bool = tf.logical_or(ignored_bool, tf.equal(self.labels, ign_label)) 258 | 259 | # Collect logits and labels that are not ignored 260 | inds = tf.squeeze(tf.where(tf.logical_not(ignored_bool))) 261 | new_logits_region_coarse = tf.gather(self.logits_region_coarse, inds, axis=0) 262 | new_logits_region_s0 = tf.gather(self.logits_region_s0, inds, axis=0) 263 | new_logits_region_s1 = tf.gather(self.logits_region_s1, inds, axis=0) 264 | new_logits_edge_coarse = tf.gather(self.logits_edge_coarse, inds, axis=0) 265 | new_logits_edge_s0 = tf.gather(self.logits_edge_s0, inds, axis=0) 266 | new_logits_edge_s1 = tf.gather(self.logits_edge_s1, inds, axis=0) 267 | 268 | new_side_0_logits_edge = tf.gather(self.side_0_logits_edge, inds, axis=0) 269 | new_side_1_logits_edge = tf.gather(self.side_1_logits_edge, inds, axis=0) 270 | new_side_2_logits_edge = tf.gather(self.side_2_logits_edge, inds, axis=0) 271 | new_side_3_logits_edge = tf.gather(self.side_3_logits_edge, inds, axis=0) 272 | new_side_4_logits_edge = tf.gather(self.side_4_logits_edge, inds, axis=0) 273 | 274 | # new input 275 | new_dict = {'point_labels': tf.gather(self.labels, inds, axis=0)} 276 | new_dict['point_boundaries'] = tf.gather(self.boundaries, inds, axis=0) 277 | new_dict['point_b_c_0'] = tf.gather(self.b_c_0, inds, axis=0) 278 | new_dict['point_b_c_1'] = tf.gather(self.b_c_1, inds, axis=0) 279 | new_dict['point_b_c_2'] = tf.gather(self.b_c_2, inds, axis=0) 280 | 281 | # Reduce label values in the range of logit shape 282 | reducing_list = tf.range(self.config.num_classes, dtype=tf.int32) 283 | inserted_value = tf.zeros((1,), dtype=tf.int32) 284 | inserted_value = inserted_value - 1 285 | 286 | for ign_label in self.config.ignored_label_inds: 287 | reducing_list = tf.concat([reducing_list[:ign_label], inserted_value, reducing_list[ign_label:]], 0) 288 | 289 | new_dict['point_labels'] = tf.gather(reducing_list, new_dict['point_labels']) 290 | # -1 indicates empty in point_b_c_i 291 | new_dict['point_b_c_0'] = tf.gather(reducing_list, new_dict['point_b_c_0']) 292 | new_dict['point_b_c_1'] = tf.gather(reducing_list, new_dict['point_b_c_1']) 293 | new_dict['point_b_c_2'] = tf.gather(reducing_list, new_dict['point_b_c_2']) 294 | 295 | # Add batch weigths to dict if needed 296 | if self.config.batch_averaged_loss: 297 | new_dict['batch_weights'] = self.inputs['batch_weights'] 298 | 299 | # Output loss 300 | # region branch 301 | loss_region_coarse = segmentation_loss_region(new_logits_region_coarse, 302 | new_dict, 303 | batch_average=self.config.batch_averaged_loss) 304 | loss_region_s0 = segmentation_loss_region(new_logits_region_s0, 305 | new_dict, 306 | batch_average=self.config.batch_averaged_loss) 307 | loss_region_s1 = segmentation_loss_region(new_logits_region_s1, 308 | new_dict, 309 | batch_average=self.config.batch_averaged_loss) 310 | loss_efr_s0 = segmentation_loss_efr(self.edge_map_fr_s0, 311 | self.inputs, 312 | batch_average=self.config.batch_averaged_loss, 313 | scannet=True, 314 | inds=inds) 315 | loss_efr_s1 = segmentation_loss_efr(self.edge_map_fr_s1, 316 | self.inputs, 317 | batch_average=self.config.batch_averaged_loss, 318 | scannet=True, 319 | inds=inds) 320 | 321 | 322 | # edge branch 323 | loss_edge_coarse = segmentation_loss_edge(new_logits_edge_coarse, 324 | new_dict, 325 | batch_average=self.config.batch_averaged_loss) 326 | loss_edge_s0 = segmentation_loss_edge(new_logits_edge_s0, 327 | new_dict, 328 | batch_average=self.config.batch_averaged_loss) 329 | loss_edge_s1 = segmentation_loss_edge(new_logits_edge_s1, 330 | new_dict, 331 | batch_average=self.config.batch_averaged_loss) 332 | loss_side_0_edge = bce_loss(new_side_0_logits_edge, 333 | new_dict, 334 | batch_average=self.config.batch_averaged_loss) 335 | loss_side_1_edge = bce_loss(new_side_1_logits_edge, 336 | new_dict, 337 | batch_average=self.config.batch_averaged_loss) 338 | loss_side_2_edge = bce_loss(new_side_2_logits_edge, 339 | new_dict, 340 | batch_average=self.config.batch_averaged_loss) 341 | loss_side_3_edge = segmentation_loss_region(new_side_3_logits_edge, 342 | new_dict, 343 | batch_average=self.config.batch_averaged_loss) 344 | loss_side_4_edge = segmentation_loss_region(new_side_4_logits_edge, 345 | new_dict, 346 | batch_average=self.config.batch_averaged_loss) 347 | 348 | 349 | self.loss_region_coarse = loss_region_coarse * self.config.num_classes 350 | self.loss_region_s0 = loss_region_s0 * self.config.num_classes 351 | self.loss_region_s1 = loss_region_s1 * self.config.num_classes 352 | self.loss_efr_s0 = loss_efr_s0 353 | self.loss_efr_s1 = loss_efr_s1 354 | self.loss_edge_coarse = loss_edge_coarse 355 | self.loss_edge_s0 = loss_edge_s0 356 | self.loss_edge_s1 = loss_edge_s1 357 | self.loss_side = loss_side_0_edge + loss_side_1_edge + loss_side_2_edge + loss_side_3_edge + loss_side_4_edge 358 | 359 | else: 360 | # region branch 361 | loss_region_coarse = segmentation_loss_region(self.logits_region_coarse, 362 | self.inputs, 363 | batch_average=self.config.batch_averaged_loss) 364 | loss_region_s0 = segmentation_loss_region(self.logits_region_s0, 365 | self.inputs, 366 | batch_average=self.config.batch_averaged_loss) 367 | loss_region_s1 = segmentation_loss_region(self.logits_region_s1, 368 | self.inputs, 369 | batch_average=self.config.batch_averaged_loss) 370 | loss_efr_s0 = segmentation_loss_efr(self.edge_map_fr_s0, 371 | self.inputs, 372 | batch_average=self.config.batch_averaged_loss) 373 | loss_efr_s1 = segmentation_loss_efr(self.edge_map_fr_s1, 374 | self.inputs, 375 | batch_average=self.config.batch_averaged_loss) 376 | 377 | # edge branch 378 | loss_edge_coarse = segmentation_loss_edge(self.logits_edge_coarse, 379 | self.inputs, 380 | batch_average=self.config.batch_averaged_loss) 381 | loss_edge_s0 = segmentation_loss_edge(self.logits_edge_s0, 382 | self.inputs, 383 | batch_average=self.config.batch_averaged_loss) 384 | loss_edge_s1 = segmentation_loss_edge(self.logits_edge_s1, 385 | self.inputs, 386 | batch_average=self.config.batch_averaged_loss) 387 | loss_side_0_edge = bce_loss(self.side_0_logits_edge, 388 | self.inputs, 389 | batch_average=self.config.batch_averaged_loss) 390 | loss_side_1_edge = bce_loss(self.side_1_logits_edge, 391 | self.inputs, 392 | batch_average=self.config.batch_averaged_loss) 393 | loss_side_2_edge = bce_loss(self.side_2_logits_edge, 394 | self.inputs, 395 | batch_average=self.config.batch_averaged_loss) 396 | loss_side_3_edge = segmentation_loss_region(self.side_3_logits_edge, 397 | self.inputs, 398 | batch_average=self.config.batch_averaged_loss) 399 | loss_side_4_edge = segmentation_loss_region(self.side_4_logits_edge, 400 | self.inputs, 401 | batch_average=self.config.batch_averaged_loss) 402 | 403 | self.loss_region_coarse = loss_region_coarse * self.config.num_classes 404 | self.loss_region_s0 = loss_region_s0 * self.config.num_classes 405 | self.loss_region_s1 = loss_region_s1 * self.config.num_classes 406 | self.loss_efr_s0 = loss_efr_s0 407 | self.loss_efr_s1 = loss_efr_s1 408 | self.loss_edge_coarse = loss_edge_coarse 409 | self.loss_edge_s0 = loss_edge_s0 410 | self.loss_edge_s1 = loss_edge_s1 411 | self.loss_side = loss_side_0_edge + loss_side_1_edge + loss_side_2_edge + loss_side_3_edge + loss_side_4_edge 412 | 413 | # Add regularization 414 | self.loss_coarse = self.loss_region_coarse + self.loss_edge_coarse + self.loss_side 415 | self.loss_refine = self.loss_region_s0 + self.loss_region_s1 + self.loss_edge_s0 + self.loss_edge_s1 + self.loss_efr_s0 + self.loss_efr_s1 416 | self.loss = self.loss_coarse + self.loss_refine + self.regularization_losses() 417 | 418 | return 419 | 420 | def regularization_losses(self): 421 | 422 | ##################### 423 | # Regularization loss 424 | ##################### 425 | 426 | # Get L2 norm of all weights 427 | regularization_losses = [tf.nn.l2_loss(v) for v in tf.global_variables() if 'weights' in v.name] 428 | self.regularization_loss = self.config.weights_decay * tf.add_n(regularization_losses) 429 | 430 | ############################## 431 | # Gaussian regularization loss 432 | ############################## 433 | 434 | gaussian_losses = [] 435 | for v in tf.global_variables(): 436 | if 'kernel_extents' in v.name: 437 | 438 | # Layer index 439 | layer = int(v.name.split('/')[1].split('_')[-1]) 440 | 441 | # Radius of convolution for this layer 442 | conv_radius = self.config.first_subsampling_dl * self.config.density_parameter * (2 ** (layer - 1)) 443 | 444 | # Target extent 445 | target_extent = conv_radius / 1.5 446 | gaussian_losses += [tf.nn.l2_loss(v - target_extent)] 447 | 448 | if len(gaussian_losses) > 0: 449 | self.gaussian_loss = self.config.gaussian_decay * tf.add_n(gaussian_losses) 450 | else: 451 | self.gaussian_loss = tf.constant(0, dtype=tf.float32) 452 | 453 | return self.gaussian_loss + self.regularization_loss 454 | 455 | def parameters_log(self): 456 | 457 | self.config.save(self.saving_path) 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | -------------------------------------------------------------------------------- /JSENet_code/test_model.py: -------------------------------------------------------------------------------- 1 | # Common libs 2 | import time 3 | import os 4 | import numpy as np 5 | import argparse 6 | 7 | # My libs 8 | from utils.config import Config 9 | from utils.tester import ModelTester 10 | 11 | # models 12 | from models.JSENet import JSENet 13 | 14 | # Datasets 15 | from dataset_modules.S3DIS import S3DISDataset 16 | from dataset_modules.Scannet import ScannetDataset 17 | 18 | 19 | # select testing semantic segmentation task or semantic edge detection task 20 | parser = argparse.ArgumentParser() 21 | parser.add_argument('--task', default='SS',help="SS/SED") 22 | FLAGS = parser.parse_args() 23 | # ---------------------------------------------------------------------------------------------------------------------- 24 | # 25 | # Utility functions 26 | # \***********************/ 27 | # 28 | 29 | 30 | def test_caller(path, step_ind, on_val): 31 | 32 | ########################## 33 | # Initiate the environment 34 | ########################## 35 | 36 | # Choose which gpu to use 37 | GPU_ID = '0' 38 | 39 | # Set GPU visible device 40 | os.environ['CUDA_VISIBLE_DEVICES'] = GPU_ID 41 | 42 | # Disable warnings 43 | os.environ['TF_CPP_MIN_LOG_LEVEL'] = '0' 44 | 45 | ########################### 46 | # Load the model parameters 47 | ########################### 48 | 49 | # Load model parameters 50 | config = Config() 51 | config.load(path) 52 | 53 | ################################## 54 | # Change model parameters for test 55 | ################################## 56 | 57 | # Change parameters for the test here. For example, you can stop augmenting the input data. 58 | config.validation_size = 500 59 | 60 | 61 | ############## 62 | # Prepare Data 63 | ############## 64 | 65 | print() 66 | print('Dataset Preparation') 67 | print('*******************') 68 | 69 | # Initiate dataset configuration 70 | if config.dataset == 'S3DIS': 71 | dataset = S3DISDataset(config.input_threads) 72 | on_val = True 73 | elif config.dataset == 'Scannet': 74 | dataset = ScannetDataset(config.input_threads, load_test=(not on_val)) 75 | else: 76 | raise ValueError('Unsupported dataset : ' + config.dataset) 77 | 78 | # Create subsample clouds of the models 79 | dl0 = config.first_subsampling_dl 80 | dataset.load_subsampled_clouds(dl0) 81 | 82 | # Initialize input pipelines 83 | if on_val: 84 | dataset.init_input_pipeline(config) 85 | else: 86 | dataset.init_test_input_pipeline(config) 87 | 88 | 89 | ############## 90 | # Define Model 91 | ############## 92 | 93 | print('Creating Model') 94 | print('**************\n') 95 | t1 = time.time() 96 | 97 | model = JSENet(dataset.flat_inputs, config) 98 | 99 | # Find all snapshot in the chosen training folder 100 | snap_path = os.path.join(path, 'snapshots') 101 | snap_steps = [int(f[:-5].split('-')[-1]) for f in os.listdir(snap_path) if f[-5:] == '.meta'] 102 | 103 | # Find which snapshot to restore 104 | chosen_step = np.sort(snap_steps)[step_ind] 105 | chosen_snap = os.path.join(path, 'snapshots', 'snap-{:d}'.format(chosen_step)) 106 | 107 | # Create a tester class 108 | if FLAGS.task == 'SS': 109 | tester = ModelTester(model, restore_snap=chosen_snap, task='SS') 110 | else: 111 | tester = ModelTester(model, restore_snap=chosen_snap, task='SED') 112 | t2 = time.time() 113 | 114 | print('\n----------------') 115 | print('Done in {:.1f} s'.format(t2 - t1)) 116 | print('----------------\n') 117 | 118 | ############ 119 | # Start test 120 | ############ 121 | 122 | print('Start Test') 123 | print('**********\n') 124 | 125 | if FLAGS.task == 'SS': 126 | if config.dataset.startswith('S3DIS'): 127 | tester.test_SS_on_val(model, dataset) 128 | elif config.dataset.startswith('Scannet'): 129 | if on_val: 130 | tester.test_SS_on_val(model, dataset) 131 | else: 132 | tester.test_SS(model, dataset) 133 | else: 134 | raise ValueError('Unsupported dataset') 135 | else: 136 | if config.dataset.startswith('S3DIS'): 137 | tester.test_SED_on_val(model, dataset) 138 | elif config.dataset.startswith('Scannet'): 139 | if on_val: 140 | tester.test_SED_on_val(model, dataset) 141 | else: 142 | raise ValueError('SED task can only be tested on validation set since gt data of Scannet test set is not available') 143 | else: 144 | raise ValueError('Unsupported dataset') 145 | 146 | 147 | # ---------------------------------------------------------------------------------------------------------------------- 148 | # 149 | # Main Call 150 | # \***************/ 151 | # 152 | 153 | 154 | if __name__ == '__main__': 155 | 156 | ########################## 157 | # Choose the model to test 158 | ########################## 159 | 160 | chosen_log = 'results/S3DIS_pre' 161 | 162 | # 163 | # You can also choose the index of the snapshot to load (last by default) 164 | # 165 | 166 | chosen_snapshot = -1 167 | 168 | # 169 | # Eventually, you can choose to test your model on the validation set 170 | # 171 | 172 | on_val = True 173 | 174 | # 175 | # If you want to modify certain parameters in the Config class, for example, to stop augmenting the input data, 176 | # there is a section for it in the function "test_caller" defined above. 177 | # 178 | 179 | ########################### 180 | # Call the test initializer 181 | ########################### 182 | 183 | # Check if log exists 184 | if not os.path.exists(chosen_log): 185 | raise ValueError('The given log does not exists: ' + chosen_log) 186 | 187 | # Let's go 188 | test_caller(chosen_log, chosen_snapshot, on_val) 189 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /JSENet_code/tf_custom_ops/compile_op.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Get TF variables 4 | TF_INC=$(python3 -c 'import tensorflow as tf; print(tf.sysconfig.get_include())') 5 | TF_LIB=$(python3 -c 'import tensorflow as tf; print(tf.sysconfig.get_lib())') 6 | 7 | # Neighbors op 8 | g++ -std=c++11 -shared tf_neighbors/tf_neighbors.cpp tf_neighbors/neighbors/neighbors.cpp cpp_utils/cloud/cloud.cpp -o tf_neighbors.so -fPIC -I$TF_INC -I$TF_INC/external/nsync/public -L$TF_LIB -ltensorflow_framework -O2 #-D_GLIBCXX_USE_CXX11_ABI=0 9 | g++ -std=c++11 -shared tf_neighbors/tf_batch_neighbors.cpp tf_neighbors/neighbors/neighbors.cpp cpp_utils/cloud/cloud.cpp -o tf_batch_neighbors.so -fPIC -I$TF_INC -I$TF_INC/external/nsync/public -L$TF_LIB -ltensorflow_framework -O2 #-D_GLIBCXX_USE_CXX11_ABI=0 10 | 11 | # Subsampling op 12 | g++ -std=c++11 -shared tf_subsampling/tf_subsampling.cpp tf_subsampling/grid_subsampling/grid_subsampling.cpp cpp_utils/cloud/cloud.cpp -o tf_subsampling.so -fPIC -I$TF_INC -I$TF_INC/external/nsync/public -L$TF_LIB -ltensorflow_framework -O2 #-D_GLIBCXX_USE_CXX11_ABI=0 13 | g++ -std=c++11 -shared tf_subsampling/tf_batch_subsampling.cpp tf_subsampling/grid_subsampling/grid_subsampling.cpp cpp_utils/cloud/cloud.cpp -o tf_batch_subsampling.so -fPIC -I$TF_INC -I$TF_INC/external/nsync/public -L$TF_LIB -ltensorflow_framework -O2 #-D_GLIBCXX_USE_CXX11_ABI=0 14 | -------------------------------------------------------------------------------- /JSENet_code/tf_custom_ops/cpp_utils/cloud/cloud.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // 3 | // 0==========================0 4 | // | Local feature test | 5 | // 0==========================0 6 | // 7 | // version 1.0 : 8 | // > 9 | // 10 | //--------------------------------------------------- 11 | // 12 | // Cloud source : 13 | // Define usefull Functions/Methods 14 | // 15 | //---------------------------------------------------- 16 | // 17 | // Hugues THOMAS - 10/02/2017 18 | // 19 | 20 | 21 | #include "cloud.h" 22 | 23 | 24 | // Getters 25 | // ******* 26 | 27 | PointXYZ max_point(std::vector points) 28 | { 29 | // Initiate limits 30 | PointXYZ maxP(points[0]); 31 | 32 | // Loop over all points 33 | for (auto p : points) 34 | { 35 | if (p.x > maxP.x) 36 | maxP.x = p.x; 37 | 38 | if (p.y > maxP.y) 39 | maxP.y = p.y; 40 | 41 | if (p.z > maxP.z) 42 | maxP.z = p.z; 43 | } 44 | 45 | return maxP; 46 | } 47 | 48 | PointXYZ min_point(std::vector points) 49 | { 50 | // Initiate limits 51 | PointXYZ minP(points[0]); 52 | 53 | // Loop over all points 54 | for (auto p : points) 55 | { 56 | if (p.x < minP.x) 57 | minP.x = p.x; 58 | 59 | if (p.y < minP.y) 60 | minP.y = p.y; 61 | 62 | if (p.z < minP.z) 63 | minP.z = p.z; 64 | } 65 | 66 | return minP; 67 | } -------------------------------------------------------------------------------- /JSENet_code/tf_custom_ops/cpp_utils/cloud/cloud.h: -------------------------------------------------------------------------------- 1 | // 2 | // 3 | // 0==========================0 4 | // | Local feature test | 5 | // 0==========================0 6 | // 7 | // version 1.0 : 8 | // > 9 | // 10 | //--------------------------------------------------- 11 | // 12 | // Cloud header 13 | // 14 | //---------------------------------------------------- 15 | // 16 | // Hugues THOMAS - 10/02/2017 17 | // 18 | 19 | 20 | # pragma once 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | #include 32 | 33 | 34 | 35 | 36 | // Point class 37 | // *********** 38 | 39 | 40 | class PointXYZ 41 | { 42 | public: 43 | 44 | // Elements 45 | // ******** 46 | 47 | float x, y, z; 48 | 49 | 50 | // Methods 51 | // ******* 52 | 53 | // Constructor 54 | PointXYZ() { x = 0; y = 0; z = 0; } 55 | PointXYZ(float x0, float y0, float z0) { x = x0; y = y0; z = z0; } 56 | 57 | // array type accessor 58 | float operator [] (int i) const 59 | { 60 | if (i == 0) return x; 61 | else if (i == 1) return y; 62 | else return z; 63 | } 64 | 65 | // opperations 66 | float dot(const PointXYZ P) const 67 | { 68 | return x * P.x + y * P.y + z * P.z; 69 | } 70 | 71 | float sq_norm() 72 | { 73 | return x*x + y*y + z*z; 74 | } 75 | 76 | PointXYZ cross(const PointXYZ P) const 77 | { 78 | return PointXYZ(y*P.z - z*P.y, z*P.x - x*P.z, x*P.y - y*P.x); 79 | } 80 | 81 | PointXYZ& operator+=(const PointXYZ& P) 82 | { 83 | x += P.x; 84 | y += P.y; 85 | z += P.z; 86 | return *this; 87 | } 88 | 89 | PointXYZ& operator-=(const PointXYZ& P) 90 | { 91 | x -= P.x; 92 | y -= P.y; 93 | z -= P.z; 94 | return *this; 95 | } 96 | 97 | PointXYZ& operator*=(const float& a) 98 | { 99 | x *= a; 100 | y *= a; 101 | z *= a; 102 | return *this; 103 | } 104 | 105 | }; 106 | 107 | 108 | // Point Opperations 109 | // ***************** 110 | 111 | inline PointXYZ operator + (const PointXYZ A, const PointXYZ B) 112 | { 113 | return PointXYZ(A.x + B.x, A.y + B.y, A.z + B.z); 114 | } 115 | 116 | inline PointXYZ operator - (const PointXYZ A, const PointXYZ B) 117 | { 118 | return PointXYZ(A.x - B.x, A.y - B.y, A.z - B.z); 119 | } 120 | 121 | inline PointXYZ operator * (const PointXYZ P, const float a) 122 | { 123 | return PointXYZ(P.x * a, P.y * a, P.z * a); 124 | } 125 | 126 | inline PointXYZ operator * (const float a, const PointXYZ P) 127 | { 128 | return PointXYZ(P.x * a, P.y * a, P.z * a); 129 | } 130 | 131 | inline std::ostream& operator << (std::ostream& os, const PointXYZ P) 132 | { 133 | return os << "[" << P.x << ", " << P.y << ", " << P.z << "]"; 134 | } 135 | 136 | inline bool operator == (const PointXYZ A, const PointXYZ B) 137 | { 138 | return A.x == B.x && A.y == B.y && A.z == B.z; 139 | } 140 | 141 | inline PointXYZ floor(const PointXYZ P) 142 | { 143 | return PointXYZ(floor(P.x), floor(P.y), floor(P.z)); 144 | } 145 | 146 | 147 | PointXYZ max_point(std::vector points); 148 | PointXYZ min_point(std::vector points); 149 | 150 | 151 | struct PointCloud 152 | { 153 | 154 | std::vector pts; 155 | 156 | // Must return the number of data points 157 | inline size_t kdtree_get_point_count() const { return pts.size(); } 158 | 159 | // Returns the dim'th component of the idx'th point in the class: 160 | // Since this is inlined and the "dim" argument is typically an immediate value, the 161 | // "if/else's" are actually solved at compile time. 162 | inline float kdtree_get_pt(const size_t idx, const size_t dim) const 163 | { 164 | if (dim == 0) return pts[idx].x; 165 | else if (dim == 1) return pts[idx].y; 166 | else return pts[idx].z; 167 | } 168 | 169 | // Optional bounding-box computation: return false to default to a standard bbox computation loop. 170 | // Return true if the BBOX was already computed by the class and returned in "bb" so it can be avoided to redo it again. 171 | // Look at bb.size() to find out the expected dimensionality (e.g. 2 or 3 for point clouds) 172 | template 173 | bool kdtree_get_bbox(BBOX& /* bb */) const { return false; } 174 | 175 | }; 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | -------------------------------------------------------------------------------- /JSENet_code/tf_custom_ops/tf_neighbors/neighbors/neighbors.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "neighbors.h" 3 | 4 | 5 | void brute_neighbors(vector& queries, vector& supports, vector& neighbors_indices, float radius, int verbose) 6 | { 7 | 8 | // Initiate variables 9 | // ****************** 10 | 11 | // square radius 12 | float r2 = radius * radius; 13 | 14 | // indices 15 | int i0 = 0; 16 | 17 | // Counting vector 18 | int max_count = 0; 19 | vector> tmp(queries.size()); 20 | 21 | // Search neigbors indices 22 | // *********************** 23 | 24 | for (auto& p0 : queries) 25 | { 26 | int i = 0; 27 | for (auto& p : supports) 28 | { 29 | if ((p0 - p).sq_norm() < r2) 30 | { 31 | tmp[i0].push_back(i); 32 | if (tmp[i0].size() > max_count) 33 | max_count = tmp[i0].size(); 34 | } 35 | i++; 36 | } 37 | i0++; 38 | } 39 | 40 | // Reserve the memory 41 | neighbors_indices.resize(queries.size() * max_count); 42 | i0 = 0; 43 | for (auto& inds : tmp) 44 | { 45 | for (int j = 0; j < max_count; j++) 46 | { 47 | if (j < inds.size()) 48 | neighbors_indices[i0 * max_count + j] = inds[j]; 49 | else 50 | neighbors_indices[i0 * max_count + j] = -1; 51 | } 52 | i0++; 53 | } 54 | 55 | return; 56 | } 57 | 58 | void ordered_neighbors(vector& queries, 59 | vector& supports, 60 | vector& neighbors_indices, 61 | float radius) 62 | { 63 | 64 | // Initiate variables 65 | // ****************** 66 | 67 | // square radius 68 | float r2 = radius * radius; 69 | 70 | // indices 71 | int i0 = 0; 72 | 73 | // Counting vector 74 | int max_count = 0; 75 | float d2; 76 | vector> tmp(queries.size()); 77 | vector> dists(queries.size()); 78 | 79 | // Search neigbors indices 80 | // *********************** 81 | 82 | for (auto& p0 : queries) 83 | { 84 | int i = 0; 85 | for (auto& p : supports) 86 | { 87 | d2 = (p0 - p).sq_norm(); 88 | if (d2 < r2) 89 | { 90 | // Find order of the new point 91 | auto it = std::upper_bound(dists[i0].begin(), dists[i0].end(), d2); 92 | int index = std::distance(dists[i0].begin(), it); 93 | 94 | // Insert element 95 | dists[i0].insert(it, d2); 96 | tmp[i0].insert(tmp[i0].begin() + index, i); 97 | 98 | // Update max count 99 | if (tmp[i0].size() > max_count) 100 | max_count = tmp[i0].size(); 101 | } 102 | i++; 103 | } 104 | i0++; 105 | } 106 | 107 | // Reserve the memory 108 | neighbors_indices.resize(queries.size() * max_count); 109 | i0 = 0; 110 | for (auto& inds : tmp) 111 | { 112 | for (int j = 0; j < max_count; j++) 113 | { 114 | if (j < inds.size()) 115 | neighbors_indices[i0 * max_count + j] = inds[j]; 116 | else 117 | neighbors_indices[i0 * max_count + j] = -1; 118 | } 119 | i0++; 120 | } 121 | 122 | return; 123 | } 124 | 125 | void batch_ordered_neighbors(vector& queries, 126 | vector& supports, 127 | vector& q_batches, 128 | vector& s_batches, 129 | vector& neighbors_indices, 130 | float radius) 131 | { 132 | 133 | // Initiate variables 134 | // ****************** 135 | 136 | // square radius 137 | float r2 = radius * radius; 138 | 139 | // indices 140 | int i0 = 0; 141 | 142 | // Counting vector 143 | int max_count = 0; 144 | float d2; 145 | vector> tmp(queries.size()); 146 | vector> dists(queries.size()); 147 | 148 | // batch index 149 | int b = 0; 150 | int sum_qb = 0; 151 | int sum_sb = 0; 152 | 153 | 154 | // Search neigbors indices 155 | // *********************** 156 | 157 | for (auto& p0 : queries) 158 | { 159 | // Check if we changed batch 160 | if (i0 == sum_qb + q_batches[b]) 161 | { 162 | sum_qb += q_batches[b]; 163 | sum_sb += s_batches[b]; 164 | b++; 165 | } 166 | 167 | // Loop only over the supports of current batch 168 | vector::iterator p_it; 169 | int i = 0; 170 | for(p_it = supports.begin() + sum_sb; p_it < supports.begin() + sum_sb + s_batches[b]; p_it++ ) 171 | { 172 | d2 = (p0 - *p_it).sq_norm(); 173 | if (d2 < r2) 174 | { 175 | // Find order of the new point 176 | auto it = std::upper_bound(dists[i0].begin(), dists[i0].end(), d2); 177 | int index = std::distance(dists[i0].begin(), it); 178 | 179 | // Insert element 180 | dists[i0].insert(it, d2); 181 | tmp[i0].insert(tmp[i0].begin() + index, sum_sb + i); 182 | 183 | // Update max count 184 | if (tmp[i0].size() > max_count) 185 | max_count = tmp[i0].size(); 186 | } 187 | i++; 188 | } 189 | i0++; 190 | } 191 | 192 | // Reserve the memory 193 | neighbors_indices.resize(queries.size() * max_count); 194 | i0 = 0; 195 | for (auto& inds : tmp) 196 | { 197 | for (int j = 0; j < max_count; j++) 198 | { 199 | if (j < inds.size()) 200 | neighbors_indices[i0 * max_count + j] = inds[j]; 201 | else 202 | neighbors_indices[i0 * max_count + j] = supports.size(); 203 | } 204 | i0++; 205 | } 206 | 207 | return; 208 | } 209 | 210 | 211 | void batch_nanoflann_neighbors(vector& queries, 212 | vector& supports, 213 | vector& q_batches, 214 | vector& s_batches, 215 | vector& neighbors_indices, 216 | float radius) 217 | { 218 | 219 | // Initiate variables 220 | // ****************** 221 | 222 | // indices 223 | int i0 = 0; 224 | 225 | // Square radius 226 | float r2 = radius * radius; 227 | 228 | // Counting vector 229 | int max_count = 0; 230 | float d2; 231 | vector>> all_inds_dists(queries.size()); 232 | 233 | // batch index 234 | int b = 0; 235 | int sum_qb = 0; 236 | int sum_sb = 0; 237 | 238 | // Nanoflann related variables 239 | // *************************** 240 | 241 | // CLoud variable 242 | PointCloud current_cloud; 243 | 244 | // Tree parameters 245 | nanoflann::KDTreeSingleIndexAdaptorParams tree_params(10 /* max leaf */); 246 | 247 | // KDTree type definition 248 | typedef nanoflann::KDTreeSingleIndexAdaptor< nanoflann::L2_Simple_Adaptor , 249 | PointCloud, 250 | 3 > my_kd_tree_t; 251 | 252 | // Pointer to trees 253 | my_kd_tree_t* index; 254 | 255 | // Build KDTree for the first batch element 256 | current_cloud.pts = vector(supports.begin() + sum_sb, supports.begin() + sum_sb + s_batches[b]); 257 | index = new my_kd_tree_t(3, current_cloud, tree_params); 258 | index->buildIndex(); 259 | 260 | 261 | // Search neigbors indices 262 | // *********************** 263 | 264 | // Search params 265 | nanoflann::SearchParams search_params; 266 | search_params.sorted = true; 267 | 268 | for (auto& p0 : queries) 269 | { 270 | 271 | // Check if we changed batch 272 | if (i0 == sum_qb + q_batches[b]) 273 | { 274 | sum_qb += q_batches[b]; 275 | sum_sb += s_batches[b]; 276 | b++; 277 | 278 | // Change the points 279 | current_cloud.pts.clear(); 280 | current_cloud.pts = vector(supports.begin() + sum_sb, supports.begin() + sum_sb + s_batches[b]); 281 | 282 | // Build KDTree of the current element of the batch 283 | delete index; 284 | index = new my_kd_tree_t(3, current_cloud, tree_params); 285 | index->buildIndex(); 286 | } 287 | 288 | // Initial guess of neighbors size 289 | all_inds_dists[i0].reserve(max_count); 290 | 291 | // Find neighbors 292 | float query_pt[3] = { p0.x, p0.y, p0.z}; 293 | size_t nMatches = index->radiusSearch(query_pt, r2, all_inds_dists[i0], search_params); 294 | 295 | // Update max count 296 | if (nMatches > max_count) 297 | max_count = nMatches; 298 | 299 | // Increment query idx 300 | i0++; 301 | } 302 | 303 | // Reserve the memory 304 | neighbors_indices.resize(queries.size() * max_count); 305 | i0 = 0; 306 | sum_sb = 0; 307 | sum_qb = 0; 308 | b = 0; 309 | for (auto& inds_dists : all_inds_dists) 310 | { 311 | // Check if we changed batch 312 | if (i0 == sum_qb + q_batches[b]) 313 | { 314 | sum_qb += q_batches[b]; 315 | sum_sb += s_batches[b]; 316 | b++; 317 | } 318 | 319 | for (int j = 0; j < max_count; j++) 320 | { 321 | if (j < inds_dists.size()) 322 | neighbors_indices[i0 * max_count + j] = inds_dists[j].first + sum_sb; 323 | else 324 | neighbors_indices[i0 * max_count + j] = supports.size(); 325 | } 326 | i0++; 327 | } 328 | 329 | delete index; 330 | 331 | return; 332 | } 333 | 334 | -------------------------------------------------------------------------------- /JSENet_code/tf_custom_ops/tf_neighbors/neighbors/neighbors.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include "../../cpp_utils/cloud/cloud.h" 4 | #include "../../cpp_utils/nanoflann/nanoflann.hpp" 5 | 6 | #include 7 | #include 8 | 9 | using namespace std; 10 | 11 | 12 | void ordered_neighbors(vector& queries, 13 | vector& supports, 14 | vector& neighbors_indices, 15 | float radius); 16 | 17 | void batch_ordered_neighbors(vector& queries, 18 | vector& supports, 19 | vector& q_batches, 20 | vector& s_batches, 21 | vector& neighbors_indices, 22 | float radius); 23 | 24 | void batch_nanoflann_neighbors(vector& queries, 25 | vector& supports, 26 | vector& q_batches, 27 | vector& s_batches, 28 | vector& neighbors_indices, 29 | float radius); 30 | -------------------------------------------------------------------------------- /JSENet_code/tf_custom_ops/tf_neighbors/tf_batch_neighbors.cpp: -------------------------------------------------------------------------------- 1 | #include "tensorflow/core/framework/op.h" 2 | #include "tensorflow/core/framework/shape_inference.h" 3 | #include "tensorflow/core/framework/op_kernel.h" 4 | #include "neighbors/neighbors.h" 5 | 6 | using namespace tensorflow; 7 | 8 | REGISTER_OP("BatchOrderedNeighbors") 9 | .Input("queries: float") 10 | .Input("supports: float") 11 | .Input("q_batches: int32") 12 | .Input("s_batches: int32") 13 | .Input("radius: float") 14 | .Output("neighbors: int32") 15 | .SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) { 16 | 17 | // Create input shape container 18 | ::tensorflow::shape_inference::ShapeHandle input; 19 | 20 | // Check inputs rank 21 | TF_RETURN_IF_ERROR(c->WithRank(c->input(0), 2, &input)); 22 | TF_RETURN_IF_ERROR(c->WithRank(c->input(1), 2, &input)); 23 | TF_RETURN_IF_ERROR(c->WithRank(c->input(2), 1, &input)); 24 | TF_RETURN_IF_ERROR(c->WithRank(c->input(3), 1, &input)); 25 | 26 | // Create the output shape 27 | c->set_output(0, c->UnknownShapeOfRank(2)); 28 | 29 | return Status::OK(); 30 | }); 31 | 32 | 33 | 34 | 35 | 36 | class BatchOrderedNeighborsOp : public OpKernel { 37 | public: 38 | explicit BatchOrderedNeighborsOp(OpKernelConstruction* context) : OpKernel(context) {} 39 | 40 | void Compute(OpKernelContext* context) override 41 | { 42 | 43 | // Grab the input tensors 44 | const Tensor& queries_tensor = context->input(0); 45 | const Tensor& supports_tensor = context->input(1); 46 | const Tensor& q_batches_tensor = context->input(2); 47 | const Tensor& s_batches_tensor = context->input(3); 48 | const Tensor& radius_tensor = context->input(4); 49 | 50 | // check shapes of input and weights 51 | const TensorShape& queries_shape = queries_tensor.shape(); 52 | const TensorShape& supports_shape = supports_tensor.shape(); 53 | const TensorShape& q_batches_shape = q_batches_tensor.shape(); 54 | const TensorShape& s_batches_shape = s_batches_tensor.shape(); 55 | 56 | // check input are [N x 3] matrices 57 | DCHECK_EQ(queries_shape.dims(), 2); 58 | DCHECK_EQ(queries_shape.dim_size(1), 3); 59 | DCHECK_EQ(supports_shape.dims(), 2); 60 | DCHECK_EQ(supports_shape.dim_size(1), 3); 61 | 62 | // Check that Batch lengths are vectors and same number of batch for both query and support 63 | DCHECK_EQ(q_batches_shape.dims(), 1); 64 | DCHECK_EQ(s_batches_shape.dims(), 1); 65 | DCHECK_EQ(q_batches_shape.dim_size(0), s_batches_shape.dim_size(0)); 66 | 67 | // Points Dimensions 68 | int Nq = (int)queries_shape.dim_size(0); 69 | int Ns = (int)supports_shape.dim_size(0); 70 | 71 | // Number of batches 72 | int Nb = (int)q_batches_shape.dim_size(0); 73 | 74 | // get the data as std vector of points 75 | float radius = radius_tensor.flat().data()[0]; 76 | vector queries = vector((PointXYZ*)queries_tensor.flat().data(), 77 | (PointXYZ*)queries_tensor.flat().data() + Nq); 78 | vector supports = vector((PointXYZ*)supports_tensor.flat().data(), 79 | (PointXYZ*)supports_tensor.flat().data() + Ns); 80 | 81 | // Batches lengths 82 | vector q_batches = vector((int*)q_batches_tensor.flat().data(), 83 | (int*)q_batches_tensor.flat().data() + Nb); 84 | vector s_batches = vector((int*)s_batches_tensor.flat().data(), 85 | (int*)s_batches_tensor.flat().data() + Nb); 86 | 87 | 88 | // Create result containers 89 | vector neighbors_indices; 90 | 91 | // Compute results 92 | //batch_ordered_neighbors(queries, supports, q_batches, s_batches, neighbors_indices, radius); 93 | batch_nanoflann_neighbors(queries, supports, q_batches, s_batches, neighbors_indices, radius); 94 | 95 | // Maximal number of neighbors 96 | int max_neighbors = neighbors_indices.size() / Nq; 97 | 98 | // create output shape 99 | TensorShape output_shape; 100 | output_shape.AddDim(Nq); 101 | output_shape.AddDim(max_neighbors); 102 | 103 | // create output tensor 104 | Tensor* output = NULL; 105 | OP_REQUIRES_OK(context, context->allocate_output(0, output_shape, &output)); 106 | auto output_tensor = output->matrix(); 107 | 108 | // Fill output tensor 109 | for (int i = 0; i < output->shape().dim_size(0); i++) 110 | { 111 | for (int j = 0; j < output->shape().dim_size(1); j++) 112 | { 113 | output_tensor(i, j) = neighbors_indices[max_neighbors * i + j]; 114 | } 115 | } 116 | } 117 | }; 118 | 119 | 120 | REGISTER_KERNEL_BUILDER(Name("BatchOrderedNeighbors").Device(DEVICE_CPU), BatchOrderedNeighborsOp); -------------------------------------------------------------------------------- /JSENet_code/tf_custom_ops/tf_neighbors/tf_neighbors.cpp: -------------------------------------------------------------------------------- 1 | #include "tensorflow/core/framework/op.h" 2 | #include "tensorflow/core/framework/shape_inference.h" 3 | #include "tensorflow/core/framework/op_kernel.h" 4 | #include "neighbors/neighbors.h" 5 | 6 | using namespace tensorflow; 7 | 8 | REGISTER_OP("OrderedNeighbors") 9 | .Input("queries: float") 10 | .Input("supports: float") 11 | .Input("radius: float") 12 | .Output("neighbors: int32") 13 | .SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) { 14 | ::tensorflow::shape_inference::ShapeHandle input; 15 | TF_RETURN_IF_ERROR(c->WithRank(c->input(0), 2, &input)); 16 | c->set_output(0, input); 17 | return Status::OK(); 18 | }); 19 | 20 | 21 | 22 | 23 | 24 | class OrderedNeighborsOp : public OpKernel { 25 | public: 26 | explicit OrderedNeighborsOp(OpKernelConstruction* context) : OpKernel(context) {} 27 | 28 | void Compute(OpKernelContext* context) override 29 | { 30 | 31 | // Grab the input tensors 32 | const Tensor& queries_tensor = context->input(0); 33 | const Tensor& supports_tensor = context->input(1); 34 | const Tensor& radius_tensor = context->input(2); 35 | 36 | // check shapes of input and weights 37 | const TensorShape& queries_shape = queries_tensor.shape(); 38 | const TensorShape& supports_shape = supports_tensor.shape(); 39 | 40 | // check input are [N x 3] matrices 41 | DCHECK_EQ(queries_shape.dims(), 2); 42 | DCHECK_EQ(queries_shape.dim_size(1), 3); 43 | DCHECK_EQ(supports_shape.dims(), 2); 44 | DCHECK_EQ(supports_shape.dim_size(1), 3); 45 | 46 | // Dimensions 47 | int Nq = (int)queries_shape.dim_size(0); 48 | int Ns = (int)supports_shape.dim_size(0); 49 | 50 | // get the data as std vector of points 51 | float radius = radius_tensor.flat().data()[0]; 52 | vector queries = vector((PointXYZ*)queries_tensor.flat().data(), 53 | (PointXYZ*)queries_tensor.flat().data() + Nq); 54 | 55 | vector supports = vector((PointXYZ*)supports_tensor.flat().data(), 56 | (PointXYZ*)supports_tensor.flat().data() + Ns); 57 | 58 | // Create result containers 59 | vector neighbors_indices; 60 | 61 | // Compute results 62 | ordered_neighbors(queries, supports, neighbors_indices, radius); 63 | 64 | // Maximal number of neighbors 65 | int max_neighbors = neighbors_indices.size() / Nq; 66 | 67 | // create output shape 68 | TensorShape output_shape; 69 | output_shape.AddDim(Nq); 70 | output_shape.AddDim(max_neighbors); 71 | 72 | // create output tensor 73 | Tensor* output = NULL; 74 | OP_REQUIRES_OK(context, context->allocate_output(0, output_shape, &output)); 75 | auto output_tensor = output->matrix(); 76 | 77 | // Fill output tensor 78 | for (int i = 0; i < output->shape().dim_size(0); i++) 79 | { 80 | for (int j = 0; j < output->shape().dim_size(1); j++) 81 | { 82 | output_tensor(i, j) = neighbors_indices[max_neighbors * i + j]; 83 | } 84 | } 85 | } 86 | }; 87 | 88 | 89 | REGISTER_KERNEL_BUILDER(Name("OrderedNeighbors").Device(DEVICE_CPU), OrderedNeighborsOp); -------------------------------------------------------------------------------- /JSENet_code/tf_custom_ops/tf_subsampling/grid_subsampling/grid_subsampling.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "grid_subsampling.h" 3 | 4 | 5 | void grid_subsampling(vector& original_points, 6 | vector& subsampled_points, 7 | vector& original_features, 8 | vector& subsampled_features, 9 | vector& original_classes, 10 | vector& subsampled_classes, 11 | float sampleDl) 12 | { 13 | 14 | // Initiate variables 15 | // ****************** 16 | 17 | // Number of points in the cloud 18 | size_t N = original_points.size(); 19 | 20 | // Dimension of the features 21 | size_t fdim = original_features.size() / N; 22 | 23 | // Limits of the cloud 24 | PointXYZ minCorner = min_point(original_points); 25 | PointXYZ maxCorner = max_point(original_points); 26 | PointXYZ originCorner = floor(minCorner * (1/sampleDl)) * sampleDl; 27 | 28 | // Dimensions of the grid 29 | size_t sampleNX = (size_t)floor((maxCorner.x - originCorner.x) / sampleDl) + 1; 30 | size_t sampleNY = (size_t)floor((maxCorner.y - originCorner.y) / sampleDl) + 1; 31 | //size_t sampleNZ = (size_t)floor((maxCorner.z - originCorner.z) / sampleDl) + 1; 32 | 33 | // Check if features and classes need to be processed 34 | bool use_feature = original_features.size() > 0; 35 | bool use_classes = original_classes.size() > 0; 36 | 37 | 38 | // Create the sampled map 39 | // ********************** 40 | 41 | // Verbose parameters 42 | int i = 0; 43 | int nDisp = N / 100; 44 | 45 | // Initiate variables 46 | size_t iX, iY, iZ, mapIdx; 47 | unordered_map data; 48 | 49 | for (auto& p : original_points) 50 | { 51 | // Position of point in sample map 52 | iX = (size_t)floor((p.x - originCorner.x) / sampleDl); 53 | iY = (size_t)floor((p.y - originCorner.y) / sampleDl); 54 | iZ = (size_t)floor((p.z - originCorner.z) / sampleDl); 55 | mapIdx = iX + sampleNX*iY + sampleNX*sampleNY*iZ; 56 | 57 | // If not already created, create key 58 | if (data.count(mapIdx) < 1) 59 | data.emplace(mapIdx, SampledData(fdim)); 60 | 61 | // Fill the sample map 62 | if (use_feature && use_classes) 63 | data[mapIdx].update_all(p, original_features.begin() + i * fdim, original_classes[i]); 64 | else if (use_feature) 65 | data[mapIdx].update_features(p, original_features.begin() + i * fdim); 66 | else if (use_classes) 67 | data[mapIdx].update_classes(p, original_classes[i]); 68 | else 69 | data[mapIdx].update_points(p); 70 | 71 | // Display 72 | i++; 73 | } 74 | 75 | // Divide for barycentre and transfer to a vector 76 | subsampled_points.reserve(data.size()); 77 | if (use_feature) 78 | subsampled_features.reserve(data.size() * fdim); 79 | if (use_classes) 80 | subsampled_classes.reserve(data.size()); 81 | for (auto& v : data) 82 | { 83 | subsampled_points.push_back(v.second.point * (1.0 / v.second.count)); 84 | if (use_feature) 85 | { 86 | float count = (float)v.second.count; 87 | transform(v.second.features.begin(), 88 | v.second.features.end(), 89 | v.second.features.begin(), 90 | [count](float f) { return f / count;}); 91 | subsampled_features.insert(subsampled_features.end(),v.second.features.begin(),v.second.features.end()); 92 | } 93 | if (use_classes) 94 | subsampled_classes.push_back(max_element(v.second.labels.begin(), v.second.labels.end())->first); 95 | } 96 | return; 97 | } 98 | 99 | 100 | 101 | void batch_grid_subsampling(vector& original_points, 102 | vector& subsampled_points, 103 | vector& original_features, 104 | vector& subsampled_features, 105 | vector& original_classes, 106 | vector& subsampled_classes, 107 | vector& original_batches, 108 | vector& subsampled_batches, 109 | float sampleDl) 110 | { 111 | // Initiate variables 112 | // ****************** 113 | 114 | int b = 0; 115 | int sum_b = 0; 116 | 117 | // Loop over batches 118 | // ***************** 119 | 120 | for (b = 0; b < original_batches.size(); b++) 121 | { 122 | // Extract batch points 123 | vector b_original_points = vector(original_points.begin () + sum_b, 124 | original_points.begin () + sum_b + original_batches[b]); 125 | 126 | // Create result containers 127 | vector b_subsampled_points; 128 | vector b_subsampled_features; 129 | vector b_subsampled_classes; 130 | 131 | // Compute subsampling on current batch 132 | grid_subsampling(b_original_points, 133 | b_subsampled_points, 134 | original_features, 135 | b_subsampled_features, 136 | original_classes, 137 | b_subsampled_classes, 138 | sampleDl); 139 | 140 | // Stack batches points 141 | subsampled_points.insert(subsampled_points.end(), b_subsampled_points.begin(), b_subsampled_points.end()); 142 | 143 | // Stack new batch lengths 144 | subsampled_batches.push_back(b_subsampled_points.size()); 145 | sum_b += original_batches[b]; 146 | } 147 | 148 | return; 149 | } -------------------------------------------------------------------------------- /JSENet_code/tf_custom_ops/tf_subsampling/grid_subsampling/grid_subsampling.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include "../../cpp_utils/cloud/cloud.h" 4 | 5 | #include 6 | #include 7 | 8 | using namespace std; 9 | 10 | class SampledData 11 | { 12 | public: 13 | 14 | // Elements 15 | // ******** 16 | 17 | int count; 18 | PointXYZ point; 19 | vector features; 20 | unordered_map labels; 21 | 22 | 23 | // Methods 24 | // ******* 25 | 26 | // Constructor 27 | SampledData() 28 | { 29 | count = 0; 30 | point = PointXYZ(); 31 | } 32 | 33 | SampledData(const size_t fdim) 34 | { 35 | count = 0; 36 | point = PointXYZ(); 37 | features = vector(fdim); 38 | } 39 | 40 | // Method Update 41 | void update_all(const PointXYZ p, std::vector::iterator f_begin, const int l) 42 | { 43 | count += 1; 44 | point += p; 45 | std::transform (features.begin(), features.end(), f_begin, features.begin(), std::plus()); 46 | labels[l] += 1; 47 | return; 48 | } 49 | void update_features(const PointXYZ p, std::vector::iterator f_begin) 50 | { 51 | count += 1; 52 | point += p; 53 | std::transform (features.begin(), features.end(), f_begin, features.begin(), std::plus()); 54 | return; 55 | } 56 | void update_classes(const PointXYZ p, const int l) 57 | { 58 | count += 1; 59 | point += p; 60 | labels[l] += 1; 61 | return; 62 | } 63 | void update_points(const PointXYZ p) 64 | { 65 | count += 1; 66 | point += p; 67 | return; 68 | } 69 | }; 70 | 71 | 72 | 73 | void grid_subsampling(vector& original_points, 74 | vector& subsampled_points, 75 | vector& original_features, 76 | vector& subsampled_features, 77 | vector& original_classes, 78 | vector& subsampled_classes, 79 | float sampleDl); 80 | 81 | 82 | void batch_grid_subsampling(vector& original_points, 83 | vector& subsampled_points, 84 | vector& original_features, 85 | vector& subsampled_features, 86 | vector& original_classes, 87 | vector& subsampled_classes, 88 | vector& original_batches, 89 | vector& subsampled_batches, 90 | float sampleDl); 91 | 92 | -------------------------------------------------------------------------------- /JSENet_code/tf_custom_ops/tf_subsampling/tf_batch_subsampling.cpp: -------------------------------------------------------------------------------- 1 | #include "tensorflow/core/framework/op.h" 2 | #include "tensorflow/core/framework/shape_inference.h" 3 | #include "tensorflow/core/framework/op_kernel.h" 4 | #include "grid_subsampling/grid_subsampling.h" 5 | 6 | using namespace tensorflow; 7 | 8 | REGISTER_OP("BatchGridSubsampling") 9 | .Input("points: float") 10 | .Input("batches: int32") 11 | .Input("dl: float") 12 | .Output("sub_points: float") 13 | .Output("sub_batches: int32") 14 | .SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) { 15 | ::tensorflow::shape_inference::ShapeHandle input0_shape; 16 | TF_RETURN_IF_ERROR(c->WithRank(c->input(0), 2, &input0_shape)); 17 | c->set_output(0, input0_shape); 18 | c->set_output(1, c->input(1)); 19 | return Status::OK(); 20 | }); 21 | 22 | 23 | 24 | 25 | 26 | class BatchGridSubsamplingOp : public OpKernel { 27 | public: 28 | explicit BatchGridSubsamplingOp(OpKernelConstruction* context) : OpKernel(context) {} 29 | 30 | void Compute(OpKernelContext* context) override 31 | { 32 | 33 | // Grab the input tensors 34 | const Tensor& points_tensor = context->input(0); 35 | const Tensor& batches_tensor = context->input(1); 36 | const Tensor& dl_tensor = context->input(2); 37 | 38 | // check shapes of input and weights 39 | const TensorShape& points_shape = points_tensor.shape(); 40 | const TensorShape& batches_shape = batches_tensor.shape(); 41 | 42 | // check input is a [N x 3] matrix 43 | DCHECK_EQ(points_shape.dims(), 2); 44 | DCHECK_EQ(points_shape.dim_size(1), 3); 45 | 46 | // Check that Batch lengths is a vector 47 | DCHECK_EQ(batches_shape.dims(), 1); 48 | 49 | // Dimensions 50 | int N = (int)points_shape.dim_size(0); 51 | 52 | // Number of batches 53 | int Nb = (int)batches_shape.dim_size(0); 54 | 55 | // get the data as std vector of points 56 | float sampleDl = dl_tensor.flat().data()[0]; 57 | vector original_points = vector((PointXYZ*)points_tensor.flat().data(), 58 | (PointXYZ*)points_tensor.flat().data() + N); 59 | 60 | // Batches lengths 61 | vector batches = vector((int*)batches_tensor.flat().data(), 62 | (int*)batches_tensor.flat().data() + Nb); 63 | 64 | // Unsupported label and features 65 | vector original_features; 66 | vector original_classes; 67 | 68 | // Create result containers 69 | vector subsampled_points; 70 | vector subsampled_features; 71 | vector subsampled_classes; 72 | vector subsampled_batches; 73 | 74 | // Compute results 75 | batch_grid_subsampling(original_points, 76 | subsampled_points, 77 | original_features, 78 | subsampled_features, 79 | original_classes, 80 | subsampled_classes, 81 | batches, 82 | subsampled_batches, 83 | sampleDl); 84 | 85 | // Sub_points output 86 | // ***************** 87 | 88 | // create output shape 89 | TensorShape sub_points_shape; 90 | sub_points_shape.AddDim(subsampled_points.size()); 91 | sub_points_shape.AddDim(3); 92 | 93 | // create output tensor 94 | Tensor* sub_points_output = NULL; 95 | OP_REQUIRES_OK(context, context->allocate_output(0, sub_points_shape, &sub_points_output)); 96 | auto sub_points_tensor = sub_points_output->matrix(); 97 | 98 | // Fill output tensor 99 | for (int i = 0; i < subsampled_points.size(); i++) 100 | { 101 | sub_points_tensor(i, 0) = subsampled_points[i].x; 102 | sub_points_tensor(i, 1) = subsampled_points[i].y; 103 | sub_points_tensor(i, 2) = subsampled_points[i].z; 104 | } 105 | 106 | // Batch length output 107 | // ******************* 108 | 109 | // create output shape 110 | TensorShape sub_batches_shape; 111 | sub_batches_shape.AddDim(subsampled_batches.size()); 112 | 113 | // create output tensor 114 | Tensor* sub_batches_output = NULL; 115 | OP_REQUIRES_OK(context, context->allocate_output(1, sub_batches_shape, &sub_batches_output)); 116 | auto sub_batches_tensor = sub_batches_output->flat(); 117 | 118 | // Fill output tensor 119 | for (int i = 0; i < subsampled_batches.size(); i++) 120 | sub_batches_tensor(i) = subsampled_batches[i]; 121 | 122 | } 123 | }; 124 | 125 | 126 | REGISTER_KERNEL_BUILDER(Name("BatchGridSubsampling").Device(DEVICE_CPU), BatchGridSubsamplingOp); -------------------------------------------------------------------------------- /JSENet_code/tf_custom_ops/tf_subsampling/tf_subsampling.cpp: -------------------------------------------------------------------------------- 1 | #include "tensorflow/core/framework/op.h" 2 | #include "tensorflow/core/framework/shape_inference.h" 3 | #include "tensorflow/core/framework/op_kernel.h" 4 | #include "grid_subsampling/grid_subsampling.h" 5 | 6 | using namespace tensorflow; 7 | 8 | REGISTER_OP("GridSubsampling") 9 | .Input("points: float") 10 | .Input("dl: float") 11 | .Output("sub_points: float") 12 | .SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) { 13 | ::tensorflow::shape_inference::ShapeHandle input; 14 | TF_RETURN_IF_ERROR(c->WithRank(c->input(0), 2, &input)); 15 | c->set_output(0, input); 16 | return Status::OK(); 17 | }); 18 | 19 | 20 | 21 | 22 | 23 | class GridSubsamplingOp : public OpKernel { 24 | public: 25 | explicit GridSubsamplingOp(OpKernelConstruction* context) : OpKernel(context) {} 26 | 27 | void Compute(OpKernelContext* context) override 28 | { 29 | 30 | // Grab the input tensors 31 | const Tensor& points_tensor = context->input(0); 32 | const Tensor& dl_tensor = context->input(1); 33 | 34 | // check shapes of input and weights 35 | const TensorShape& points_shape = points_tensor.shape(); 36 | 37 | // check input are [N x 3] matrices 38 | DCHECK_EQ(points_shape.dims(), 2); 39 | DCHECK_EQ(points_shape.dim_size(1), 3); 40 | 41 | // Dimensions 42 | int N = (int)points_shape.dim_size(0); 43 | 44 | // get the data as std vector of points 45 | float sampleDl = dl_tensor.flat().data()[0]; 46 | vector original_points = vector((PointXYZ*)points_tensor.flat().data(), 47 | (PointXYZ*)points_tensor.flat().data() + N); 48 | 49 | // Unsupported label and features 50 | vector original_features; 51 | vector original_classes; 52 | 53 | // Create result containers 54 | vector subsampled_points; 55 | vector subsampled_features; 56 | vector subsampled_classes; 57 | 58 | // Compute results 59 | grid_subsampling(original_points, 60 | subsampled_points, 61 | original_features, 62 | subsampled_features, 63 | original_classes, 64 | subsampled_classes, 65 | sampleDl); 66 | 67 | // create output shape 68 | TensorShape output_shape; 69 | output_shape.AddDim(subsampled_points.size()); 70 | output_shape.AddDim(3); 71 | 72 | // create output tensor 73 | Tensor* output = NULL; 74 | OP_REQUIRES_OK(context, context->allocate_output(0, output_shape, &output)); 75 | auto output_tensor = output->matrix(); 76 | 77 | // Fill output tensor 78 | for (int i = 0; i < output->shape().dim_size(0); i++) 79 | { 80 | output_tensor(i, 0) = subsampled_points[i].x; 81 | output_tensor(i, 1) = subsampled_points[i].y; 82 | output_tensor(i, 2) = subsampled_points[i].z; 83 | } 84 | } 85 | }; 86 | 87 | 88 | REGISTER_KERNEL_BUILDER(Name("GridSubsampling").Device(DEVICE_CPU), GridSubsamplingOp); -------------------------------------------------------------------------------- /JSENet_code/training_S3DIS.py: -------------------------------------------------------------------------------- 1 | # 2 | # 3 | # 0=================================0 4 | # | Kernel Point Convolutions | 5 | # 0=================================0 6 | # 7 | # 8 | # ---------------------------------------------------------------------------------------------------------------------- 9 | # 10 | # Callable script to start a training on S3DIS dataset 11 | # 12 | # ---------------------------------------------------------------------------------------------------------------------- 13 | # 14 | # Hugues THOMAS - 11/06/2018 15 | # 16 | 17 | 18 | # ---------------------------------------------------------------------------------------------------------------------- 19 | # 20 | # Imports and global variables 21 | # \**********************************/ 22 | # 23 | 24 | 25 | # Common libs 26 | import time 27 | import os 28 | import sys 29 | 30 | # Custom libs 31 | from utils.config import Config 32 | from utils.trainer import ModelTrainer 33 | from models.JSENet import JSENet 34 | 35 | # Dataset 36 | from dataset_modules.S3DIS import S3DISDataset 37 | 38 | 39 | # ---------------------------------------------------------------------------------------------------------------------- 40 | # 41 | # Config Class 42 | # \******************/ 43 | # 44 | 45 | 46 | class S3DISConfig(Config): 47 | """ 48 | Override the parameters you want to modify for this dataset 49 | """ 50 | 51 | #################### 52 | # Dataset parameters 53 | #################### 54 | 55 | # Dataset name 56 | dataset = 'S3DIS' 57 | 58 | # Number of classes in the dataset (This value is overwritten by dataset class when initiating input pipeline). 59 | num_classes = None 60 | 61 | # Type of task performed on this dataset (also overwritten) 62 | network_model = None 63 | 64 | # Number of CPU threads for the input pipeline 65 | input_threads = 8 66 | 67 | ######################### 68 | # Architecture definition 69 | ######################### 70 | 71 | # Define layers 72 | architecture = ['simple', 73 | 'resnetb', 74 | 'resnetb_strided', 75 | 'resnetb', 76 | 'resnetb_strided', 77 | 'resnetb', 78 | 'resnetb_strided', 79 | 'resnetb', 80 | 'resnetb_strided', 81 | 'resnetb', 82 | 'nearest_upsample', 83 | 'unary', 84 | 'nearest_upsample', 85 | 'unary', 86 | 'nearest_upsample', 87 | 'unary', 88 | 'nearest_upsample', 89 | 'unary'] 90 | 91 | # KPConv specific parameters 92 | num_kernel_points = 15 93 | first_subsampling_dl = 0.04 94 | in_radius = 2.0 95 | 96 | # Density of neighborhoods for deformable convs (which need bigger radiuses). For normal conv we use KP_extent 97 | density_parameter = 5.0 98 | 99 | # Influence function of KPConv in ('constant', 'linear', gaussian) 100 | KP_influence = 'linear' 101 | KP_extent = 1.0 102 | 103 | # Aggregation function of KPConv in ('closest', 'sum') 104 | convolution_mode = 'sum' 105 | 106 | # Can the network learn modulations in addition to deformations 107 | modulated = False 108 | 109 | # Offset loss 110 | # 'permissive' only constrains offsets to be inside the big radius 111 | # 'fitting' helps deformed kernels to adapt to the geometry by penalizing distance to input points 112 | offsets_loss = 'fitting' 113 | offsets_decay = 0.1 114 | 115 | # Choice of input features 116 | in_features_dim = 5 117 | 118 | # Batch normalization parameters 119 | use_batch_norm = True 120 | batch_norm_momentum = 0.98 121 | 122 | ##################### 123 | # Training parameters 124 | ##################### 125 | 126 | # Maximal number of epochs 127 | max_epoch = 500 128 | 129 | # Learning rate management 130 | learning_rate = 1e-2 131 | momentum = 0.98 132 | lr_decays = {i: 0.1**(1/100) for i in range(1, max_epoch)} 133 | grad_clip_norm = 100.0 134 | 135 | # Number of batch 136 | batch_num = 4 137 | 138 | # Number of steps per epochs (cannot be None for this dataset) 139 | epoch_steps = 300 140 | 141 | # Number of validation examples per epoch 142 | validation_size = 50 143 | 144 | # Number of epoch between each snapshot 145 | snapshot_gap = 25 146 | 147 | # Augmentations 148 | augment_scale_anisotropic = True 149 | augment_symmetries = [True, False, False] 150 | augment_rotation = 'vertical' 151 | augment_scale_min = 0.8 152 | augment_scale_max = 1.2 153 | augment_noise = 0.001 154 | augment_occlusion = 'none' 155 | augment_color = 0.8 156 | 157 | # Whether to use loss averaged on all points, or averaged per batch. 158 | batch_averaged_loss = False 159 | 160 | # Do we nee to save convergence 161 | saving = True 162 | saving_path = None 163 | 164 | 165 | # ---------------------------------------------------------------------------------------------------------------------- 166 | # 167 | # Main Call 168 | # \***************/ 169 | # 170 | 171 | 172 | if __name__ == '__main__': 173 | 174 | ########################## 175 | # Initiate the environment 176 | ########################## 177 | 178 | # Choose which gpu to use 179 | GPU_ID = '0' 180 | 181 | # Set GPU visible device 182 | os.environ['CUDA_VISIBLE_DEVICES'] = GPU_ID 183 | 184 | # Enable/Disable warnings (set level to '0'/'3') 185 | os.environ['TF_CPP_MIN_LOG_LEVEL'] = '0' 186 | 187 | ########################### 188 | # Load the model parameters 189 | ########################### 190 | 191 | config = S3DISConfig() 192 | 193 | ############## 194 | # Prepare Data 195 | ############## 196 | 197 | print() 198 | print('Dataset Preparation') 199 | print('*******************') 200 | 201 | # Initiate dataset configuration 202 | dataset = S3DISDataset(config.input_threads) 203 | 204 | # Create subsampled input clouds 205 | dl0 = config.first_subsampling_dl 206 | dataset.load_subsampled_clouds(dl0) 207 | 208 | # Initialize input pipelines 209 | dataset.init_input_pipeline(config) 210 | 211 | ############## 212 | # Define Model 213 | ############## 214 | 215 | print('Creating Model') 216 | print('**************\n') 217 | t1 = time.time() 218 | 219 | # Model class 220 | model = JSENet(dataset.flat_inputs, config) 221 | 222 | # Trainer class 223 | trainer = ModelTrainer(model) 224 | t2 = time.time() 225 | 226 | print('\n----------------') 227 | print('Done in {:.1f} s'.format(t2 - t1)) 228 | print('----------------\n') 229 | 230 | ################ 231 | # Start training 232 | ################ 233 | 234 | print('Start Training') 235 | print('**************\n') 236 | 237 | trainer.train(model, dataset) 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | -------------------------------------------------------------------------------- /JSENet_code/training_Scannet.py: -------------------------------------------------------------------------------- 1 | # 2 | # 3 | # 0=================================0 4 | # | Kernel Point Convolutions | 5 | # 0=================================0 6 | # 7 | # 8 | # ---------------------------------------------------------------------------------------------------------------------- 9 | # 10 | # Callable script to start a training on S3DIS dataset 11 | # 12 | # ---------------------------------------------------------------------------------------------------------------------- 13 | # 14 | # Hugues THOMAS - 11/06/2018 15 | # 16 | 17 | 18 | # ---------------------------------------------------------------------------------------------------------------------- 19 | # 20 | # Imports and global variables 21 | # \**********************************/ 22 | # 23 | 24 | 25 | # Common libs 26 | import time 27 | import os 28 | import sys 29 | 30 | # Custom libs 31 | from utils.config import Config 32 | from utils.trainer import ModelTrainer 33 | from models.JSENet import JSENet 34 | 35 | # Dataset 36 | from dataset_modules.Scannet import ScannetDataset 37 | 38 | 39 | # ---------------------------------------------------------------------------------------------------------------------- 40 | # 41 | # Config Class 42 | # \******************/ 43 | # 44 | 45 | 46 | class ScannetConfig(Config): 47 | """ 48 | Override the parameters you want to modify for this dataset 49 | """ 50 | 51 | #################### 52 | # Dataset parameters 53 | #################### 54 | 55 | # Dataset name 56 | dataset = 'Scannet' 57 | 58 | # Number of classes in the dataset (This value is overwritten by dataset class when initiating input pipeline). 59 | num_classes = None 60 | 61 | # Type of task performed on this dataset (also overwritten) 62 | network_model = None 63 | 64 | # Number of CPU threads for the input pipeline 65 | input_threads = 8 66 | 67 | ######################### 68 | # Architecture definition 69 | ######################### 70 | 71 | # Define layers 72 | architecture = [ 73 | 'simple', 74 | 'resnetb', 75 | 'resnetb_strided', 76 | 'resnetb', 77 | 'resnetb_strided', 78 | 'resnetb', 79 | 'resnetb_strided', 80 | 'resnetb', 81 | 'resnetb_strided', 82 | 'resnetb', 83 | 'nearest_upsample', 84 | 'unary', 85 | 'nearest_upsample', 86 | 'unary', 87 | 'nearest_upsample', 88 | 'unary', 89 | 'nearest_upsample', 90 | 'unary' 91 | ] 92 | 93 | # KPConv specific parameters 94 | num_kernel_points = 15 95 | first_subsampling_dl = 0.04 96 | in_radius = 2.0 97 | 98 | # Density of neighborhoods for deformable convs (which need bigger radiuses). For normal conv we use KP_extent 99 | density_parameter = 5.0 100 | 101 | # Behavior of convolutions in ('constant', 'linear', gaussian) 102 | KP_influence = 'linear' 103 | KP_extent = 1.0 104 | 105 | # Behavior of convolutions in ('closest', 'sum') 106 | convolution_mode = 'sum' 107 | 108 | # Can the network learn modulations 109 | modulated = False 110 | 111 | # Offset loss 112 | # 'permissive' only constrains offsets inside the big radius 113 | # 'fitting' helps deformed kernels to adapt to the geometry by penalizing distance to input points 114 | offsets_loss = 'fitting' 115 | offsets_decay = 0.1 116 | 117 | # Choice of input features 118 | in_features_dim = 4 119 | 120 | # Batch normalization parameters 121 | use_batch_norm = True 122 | batch_norm_momentum = 0.98 123 | 124 | ##################### 125 | # Training parameters 126 | ##################### 127 | 128 | # Maximal number of epochs 129 | max_epoch = 500 130 | 131 | # Learning rate management 132 | learning_rate = 1e-2 133 | momentum = 0.98 134 | lr_decays = {i: 0.1**(1/100) for i in range(1, max_epoch)} 135 | grad_clip_norm = 100.0 136 | 137 | # Number of batch 138 | batch_num = 6 139 | 140 | # Number of steps per epochs (cannot be None for this dataset) 141 | epoch_steps = 600 142 | 143 | # Number of epoch between each snapshot 144 | snapshot_gap = 25 145 | 146 | # Augmentations 147 | augment_scale_anisotropic = True 148 | augment_symmetries = [True, False, False] 149 | augment_rotation = 'vertical' 150 | augment_scale_min = 0.9 151 | augment_scale_max = 1.1 152 | augment_noise = 0.001 153 | augment_occlusion = 'none' 154 | augment_color = 1.0 155 | 156 | # Whether to use loss averaged on all points, or averaged per batch. 157 | batch_averaged_loss = False 158 | 159 | # Do we nee to save convergence 160 | saving = True 161 | saving_path = None 162 | 163 | 164 | 165 | # ---------------------------------------------------------------------------------------------------------------------- 166 | # 167 | # Main Call 168 | # \***************/ 169 | # 170 | 171 | 172 | if __name__ == '__main__': 173 | 174 | ########################## 175 | # Initiate the environment 176 | ########################## 177 | 178 | # Choose which gpu to use 179 | GPU_ID = '0' 180 | 181 | # Set GPU visible device 182 | os.environ['CUDA_VISIBLE_DEVICES'] = GPU_ID 183 | 184 | # Enable/Disable warnings (set level to '0'/'3') 185 | os.environ['TF_CPP_MIN_LOG_LEVEL'] = '0' 186 | 187 | ########################### 188 | # Load the model parameters 189 | ########################### 190 | 191 | # Load config 192 | config = ScannetConfig() 193 | 194 | ############## 195 | # Prepare Data 196 | ############## 197 | 198 | print() 199 | print('Dataset Preparation') 200 | print('*******************') 201 | 202 | # Initiate dataset configuration 203 | dataset = ScannetDataset(config.input_threads, load_test=False) 204 | 205 | # Create subsampled input clouds 206 | dl0 = config.first_subsampling_dl 207 | dataset.load_subsampled_clouds(dl0) 208 | 209 | # Initialize input pipelines 210 | dataset.init_input_pipeline(config) 211 | 212 | ############## 213 | # Define Model 214 | ############## 215 | 216 | print('Creating Model') 217 | print('**************\n') 218 | t1 = time.time() 219 | 220 | # Model class 221 | model = JSENet(dataset.flat_inputs, config) 222 | 223 | # Trainer class 224 | trainer = ModelTrainer(model) 225 | t2 = time.time() 226 | 227 | print('\n----------------') 228 | print('Done in {:.1f} s'.format(t2 - t1)) 229 | print('----------------\n') 230 | 231 | ################ 232 | # Start training 233 | ################ 234 | 235 | print('Start Training') 236 | print('**************\n') 237 | 238 | trainer.train(model, dataset) 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | -------------------------------------------------------------------------------- /JSENet_code/utils/config.py: -------------------------------------------------------------------------------- 1 | # 2 | # 3 | # 0=================================0 4 | # | Kernel Point Convolutions | 5 | # 0=================================0 6 | # 7 | # 8 | # ---------------------------------------------------------------------------------------------------------------------- 9 | # 10 | # Configuration class 11 | # 12 | # ---------------------------------------------------------------------------------------------------------------------- 13 | # 14 | # Hugues THOMAS - 11/06/2018 15 | # 16 | 17 | 18 | from os.path import join 19 | 20 | 21 | class Config: 22 | """ 23 | Class containing the parameters you want to modify for this dataset 24 | """ 25 | 26 | ################## 27 | # Input parameters 28 | ################## 29 | 30 | # Dataset name 31 | dataset = '' 32 | 33 | # Type of network model 34 | network_model = '' 35 | 36 | # Number of classes in the dataset 37 | num_classes = 0 38 | 39 | # Dimension of input points 40 | in_points_dim = 3 41 | 42 | # Dimension of input features 43 | in_features_dim = 1 44 | 45 | # Radius of the input sphere (ignored for models, only used for point clouds) 46 | in_radius = 1.0 47 | 48 | # Number of CPU threads for the input pipeline 49 | input_threads = 8 50 | 51 | ################## 52 | # Model parameters 53 | ################## 54 | 55 | # Architecture definition. List of blocks 56 | architecture = [] 57 | 58 | # Dimension of the first feature maps 59 | first_features_dim = 64 60 | 61 | # Batch normalization parameters 62 | use_batch_norm = True 63 | batch_norm_momentum = 0.99 64 | 65 | # For segmentation models : ratio between the segmented area and the input area 66 | segmentation_ratio = 1.0 67 | 68 | ################### 69 | # KPConv parameters 70 | ################### 71 | 72 | # First size of grid used for subsampling 73 | first_subsampling_dl = 0.02 74 | 75 | # Radius of the kernels in the first layer (deprecated) 76 | first_kernel_radius = 0.1 77 | 78 | # Number of points in the kernels 79 | num_kernel_points = 15 80 | 81 | # density of neighbors in kernel range 82 | # For each layer, support points are subsampled on a grid with dl = kernel_radius / density_parameter 83 | density_parameter = 3.0 84 | 85 | # Kernel point influence radius 86 | KP_extent = 1.0 87 | 88 | # Influence function when d < KP_extent. ('constant', 'linear', 'gaussian') When d > KP_extent, always zero 89 | KP_influence = 'gaussian' 90 | 91 | # Behavior of convolutions in ('closest', 'sum') 92 | # Decide if you sum all kernel point influences, or if you only take the influence of the closest KP 93 | convolution_mode = 'closest' 94 | 95 | # Fixed points in the kernel : 'none', 'center' or 'verticals' 96 | fixed_kernel_points = 'center' 97 | 98 | # Can the network learn kernel dispositions (deprecated) 99 | trainable_positions = False 100 | 101 | # Use modulateion in deformable convolutions 102 | modulated = False 103 | 104 | ##################### 105 | # Training parameters 106 | ##################### 107 | 108 | # Network optimizer parameters (learning rate and momentum) 109 | learning_rate = 1e-4 110 | momentum = 0.9 111 | 112 | # Learning rate decays. Dictionary of all decay values with their epoch {epoch: decay}. 113 | lr_decays = {200: 0.2, 300: 0.2} 114 | 115 | # Gradient clipping value (negative means no clipping) 116 | grad_clip_norm = 100.0 117 | 118 | # Augmentation parameters 119 | augment_scale_anisotropic = True 120 | augment_scale_min = 0.9 121 | augment_scale_max = 1.1 122 | augment_symmetries = [False, False, False] 123 | augment_rotation = 'vertical' 124 | augment_noise = 0.005 125 | augment_occlusion = 'planar' 126 | augment_occlusion_ratio = 0.2 127 | augment_occlusion_num = 1 128 | augment_color = 0.7 129 | 130 | # Regularization loss importance 131 | weights_decay = 1e-3 132 | 133 | # Gaussian loss 134 | gaussian_decay = 1e-3 135 | 136 | # Type of output loss with regard to batches when segmentation 137 | batch_averaged_loss = False 138 | 139 | # Point loss DPRECATED 140 | points_loss = '' 141 | points_decay = 1e-2 142 | 143 | # Offset regularization loss 144 | offsets_loss = 'permissive' 145 | offsets_decay = 1e-2 146 | 147 | # Number of batch 148 | batch_num = 10 149 | 150 | # Maximal number of epochs 151 | max_epoch = 1000 152 | 153 | # Number of steps per epochs 154 | epoch_steps = 1000 155 | 156 | # Number of validation examples per epoch 157 | validation_size = 100 158 | 159 | # Number of epoch between each snapshot 160 | snapshot_gap = 50 161 | 162 | # Do we nee to save convergence 163 | saving = True 164 | saving_path = None 165 | 166 | def __init__(self): 167 | """ 168 | Class Initialyser 169 | """ 170 | 171 | # Number of layers 172 | self.num_layers = len([block for block in self.architecture if 'pool' in block or 'strided' in block]) + 1 173 | 174 | def load(self, path): 175 | 176 | filename = join(path, 'parameters.txt') 177 | with open(filename, 'r') as f: 178 | lines = f.readlines() 179 | 180 | # Class variable dictionary 181 | for line in lines: 182 | line_info = line.split() 183 | if len(line_info) > 1 and line_info[0] != '#': 184 | 185 | if line_info[2] == 'None': 186 | setattr(self, line_info[0], None) 187 | 188 | elif line_info[0] == 'lr_decay_epochs': 189 | self.lr_decays = {int(b.split(':')[0]): float(b.split(':')[1]) for b in line_info[2:]} 190 | 191 | elif line_info[0] == 'architecture': 192 | self.architecture = [b for b in line_info[2:]] 193 | 194 | elif line_info[0] == 'augment_symmetries': 195 | self.augment_symmetries = [bool(int(b)) for b in line_info[2:]] 196 | 197 | elif line_info[0] == 'num_classes': 198 | if len(line_info) > 3: 199 | self.num_classes = [int(c) for c in line_info[2:]] 200 | else: 201 | self.num_classes = int(line_info[2]) 202 | 203 | else: 204 | 205 | attr_type = type(getattr(self, line_info[0])) 206 | if attr_type == bool: 207 | setattr(self, line_info[0], attr_type(int(line_info[2]))) 208 | else: 209 | setattr(self, line_info[0], attr_type(line_info[2])) 210 | 211 | self.saving = True 212 | self.saving_path = path 213 | self.__init__() 214 | 215 | def save(self, path): 216 | 217 | with open(join(path, 'parameters.txt'), "w") as text_file: 218 | 219 | text_file.write('# -----------------------------------#\n') 220 | text_file.write('# Parameters of the training session #\n') 221 | text_file.write('# -----------------------------------#\n\n') 222 | 223 | # Input parameters 224 | text_file.write('# Input parameters\n') 225 | text_file.write('# ****************\n\n') 226 | text_file.write('dataset = {:s}\n'.format(self.dataset)) 227 | text_file.write('network_model = {:s}\n'.format(self.network_model)) 228 | if type(self.num_classes) is list: 229 | text_file.write('num_classes =') 230 | for n in self.num_classes: 231 | text_file.write(' {:d}'.format(n)) 232 | text_file.write('\n') 233 | else: 234 | text_file.write('num_classes = {:d}\n'.format(self.num_classes)) 235 | text_file.write('in_points_dim = {:d}\n'.format(self.in_points_dim)) 236 | text_file.write('in_features_dim = {:d}\n'.format(self.in_features_dim)) 237 | text_file.write('in_radius = {:.3f}\n'.format(self.in_radius)) 238 | text_file.write('input_threads = {:d}\n\n'.format(self.input_threads)) 239 | 240 | # Model parameters 241 | text_file.write('# Model parameters\n') 242 | text_file.write('# ****************\n\n') 243 | 244 | text_file.write('architecture =') 245 | for a in self.architecture: 246 | text_file.write(' {:s}'.format(a)) 247 | text_file.write('\n') 248 | text_file.write('num_layers = {:d}\n'.format(self.num_layers)) 249 | text_file.write('first_features_dim = {:d}\n'.format(self.first_features_dim)) 250 | text_file.write('use_batch_norm = {:d}\n'.format(int(self.use_batch_norm))) 251 | text_file.write('batch_norm_momentum = {:.3f}\n\n'.format(self.batch_norm_momentum)) 252 | text_file.write('segmentation_ratio = {:.3f}\n\n'.format(self.segmentation_ratio)) 253 | 254 | # KPConv parameters 255 | text_file.write('# KPConv parameters\n') 256 | text_file.write('# *****************\n\n') 257 | 258 | text_file.write('first_subsampling_dl = {:.3f}\n'.format(self.first_subsampling_dl)) 259 | text_file.write('num_kernel_points = {:d}\n'.format(self.num_kernel_points)) 260 | text_file.write('density_parameter = {:.3f}\n'.format(self.density_parameter)) 261 | text_file.write('fixed_kernel_points = {:s}\n'.format(self.fixed_kernel_points)) 262 | text_file.write('KP_extent = {:.3f}\n'.format(self.KP_extent)) 263 | text_file.write('KP_influence = {:s}\n'.format(self.KP_influence)) 264 | text_file.write('convolution_mode = {:s}\n'.format(self.convolution_mode)) 265 | text_file.write('trainable_positions = {:d}\n\n'.format(int(self.trainable_positions))) 266 | text_file.write('modulated = {:d}\n\n'.format(int(self.modulated))) 267 | 268 | # Training parameters 269 | text_file.write('# Training parameters\n') 270 | text_file.write('# *******************\n\n') 271 | 272 | text_file.write('learning_rate = {:f}\n'.format(self.learning_rate)) 273 | text_file.write('momentum = {:f}\n'.format(self.momentum)) 274 | text_file.write('lr_decay_epochs =') 275 | for e, d in self.lr_decays.items(): 276 | text_file.write(' {:d}:{:f}'.format(e, d)) 277 | text_file.write('\n') 278 | text_file.write('grad_clip_norm = {:f}\n\n'.format(self.grad_clip_norm)) 279 | 280 | 281 | text_file.write('augment_symmetries =') 282 | for a in self.augment_symmetries: 283 | text_file.write(' {:d}'.format(int(a))) 284 | text_file.write('\n') 285 | text_file.write('augment_rotation = {:s}\n'.format(self.augment_rotation)) 286 | text_file.write('augment_noise = {:f}\n'.format(self.augment_noise)) 287 | text_file.write('augment_occlusion = {:s}\n'.format(self.augment_occlusion)) 288 | text_file.write('augment_occlusion_ratio = {:.3f}\n'.format(self.augment_occlusion_ratio)) 289 | text_file.write('augment_occlusion_num = {:d}\n'.format(self.augment_occlusion_num)) 290 | text_file.write('augment_scale_anisotropic = {:d}\n'.format(int(self.augment_scale_anisotropic))) 291 | text_file.write('augment_scale_min = {:.3f}\n'.format(self.augment_scale_min)) 292 | text_file.write('augment_scale_max = {:.3f}\n'.format(self.augment_scale_max)) 293 | text_file.write('augment_color = {:.3f}\n\n'.format(self.augment_color)) 294 | 295 | text_file.write('weights_decay = {:f}\n'.format(self.weights_decay)) 296 | text_file.write('gaussian_decay = {:f}\n'.format(self.gaussian_decay)) 297 | text_file.write('batch_averaged_loss = {:d}\n'.format(int(self.batch_averaged_loss))) 298 | text_file.write('offsets_loss = {:s}\n'.format(self.offsets_loss)) 299 | text_file.write('offsets_decay = {:f}\n'.format(self.offsets_decay)) 300 | text_file.write('batch_num = {:d}\n'.format(self.batch_num)) 301 | text_file.write('max_epoch = {:d}\n'.format(self.max_epoch)) 302 | if self.epoch_steps is None: 303 | text_file.write('epoch_steps = None\n') 304 | else: 305 | text_file.write('epoch_steps = {:d}\n'.format(self.epoch_steps)) 306 | text_file.write('validation_size = {:d}\n'.format(self.validation_size)) 307 | text_file.write('snapshot_gap = {:d}\n'.format(self.snapshot_gap)) 308 | 309 | 310 | 311 | -------------------------------------------------------------------------------- /JSENet_code/utils/mesh.py: -------------------------------------------------------------------------------- 1 | # 2 | # 3 | # 0======================0 4 | # | Mesh utilities | 5 | # 0======================0 6 | # 7 | # 8 | # ---------------------------------------------------------------------------------------------------------------------- 9 | # 10 | # functions related to meshes 11 | # 12 | # ---------------------------------------------------------------------------------------------------------------------- 13 | # 14 | # Hugues THOMAS - 10/02/2017 15 | # 16 | 17 | 18 | # ---------------------------------------------------------------------------------------------------------------------- 19 | # 20 | # Imports and global variables 21 | # \**********************************/ 22 | # 23 | 24 | 25 | # Basic libs 26 | import numpy as np 27 | import time 28 | 29 | 30 | # ---------------------------------------------------------------------------------------------------------------------- 31 | # 32 | # Functions 33 | # \***************/ 34 | # 35 | 36 | 37 | def rasterize_mesh(vertices, faces, dl, verbose=False): 38 | """ 39 | Creation of point cloud from mesh via rasterization. All models are rescaled to fit in a 1 meter radius sphere 40 | :param vertices: array of vertices 41 | :param faces: array of faces 42 | :param dl: parameter controlling density. Distance between each point 43 | :param verbose: display parameter 44 | :return: point cloud 45 | """ 46 | 47 | ###################################### 48 | # Eliminate useless faces and vertices 49 | ###################################### 50 | 51 | # 3D coordinates of faces 52 | faces3D = vertices[faces, :] 53 | sides = np.stack([faces3D[:, i, :] - faces3D[:, i - 1, :] for i in [2, 0, 1]], axis=1) 54 | 55 | # Indices of big enough faces 56 | keep_bool = np.min(np.linalg.norm(sides, axis=-1), axis=-1) > 1e-9 57 | faces = faces[keep_bool] 58 | 59 | ################################## 60 | # Place random points on each face 61 | ################################## 62 | 63 | # 3D coordinates of faces 64 | faces3D = vertices[faces, :] 65 | 66 | # Area of each face 67 | opposite_sides = np.stack([faces3D[:, i, :] - faces3D[:, i - 1, :] for i in [2, 0, 1]], axis=1) 68 | lengths = np.linalg.norm(opposite_sides, axis=-1) 69 | 70 | # Points for each face 71 | all_points = [] 72 | all_vert_inds = [] 73 | for face_verts, face, l, sides in zip(faces, faces3D, lengths, opposite_sides): 74 | 75 | # All points generated for this face 76 | face_points = [] 77 | 78 | # Safe check for null faces 79 | if np.min(l) < 1e-9: 80 | continue 81 | 82 | # Smallest faces, only place one point in the center 83 | if np.max(l) < dl: 84 | face_points.append(np.mean(face, axis=0)) 85 | continue 86 | 87 | # Chose indices so that A is the largest angle 88 | A_idx = np.argmax(l) 89 | B_idx = (A_idx + 1) % 3 90 | C_idx = (A_idx + 2) % 3 91 | i = -sides[B_idx] / l[B_idx] 92 | j = sides[C_idx] / l[C_idx] 93 | 94 | # Create a mesh grid of points along the two smallest sides 95 | s1 = (l[B_idx] % dl) / 2 96 | s2 = (l[C_idx] % dl) / 2 97 | x, y = np.meshgrid(np.arange(s1, l[B_idx], dl), np.arange(s2, l[C_idx], dl)) 98 | points = face[A_idx, :] + (np.expand_dims(x.ravel(), 1) * i + np.expand_dims(y.ravel(), 1) * j) 99 | points = points[x.ravel() / l[B_idx] + y.ravel() / l[C_idx] <= 1, :] 100 | face_points.append(points) 101 | 102 | # Add points on the three edges 103 | for edge_idx in range(3): 104 | i = sides[edge_idx] / l[edge_idx] 105 | A_idx = (edge_idx + 1) % 3 106 | s1 = (l[edge_idx] % dl) / 2 107 | x = np.arange(s1, l[edge_idx], dl) 108 | points = face[A_idx, :] + np.expand_dims(x.ravel(), 1) * i 109 | face_points.append(points) 110 | 111 | # Add vertices 112 | face_points.append(face) 113 | 114 | # Compute vertex indices 115 | dists = np.sum(np.square(np.expand_dims(np.vstack(face_points), 1) - face), axis=2) 116 | all_vert_inds.append(face_verts[np.argmin(dists, axis=1)]) 117 | 118 | # Save points and inds 119 | all_points += face_points 120 | 121 | return np.vstack(all_points).astype(np.float32), np.hstack(all_vert_inds) 122 | 123 | 124 | def cylinder_mesh(cylinder, precision=24): 125 | 126 | # Get parameters 127 | center = cylinder[:3] 128 | h = cylinder[3] 129 | r = cylinder[4] 130 | 131 | # Create vertices 132 | theta = 2.0 * np.pi / precision 133 | thetas = np.arange(precision) * theta 134 | circleX = r * np.cos(thetas) 135 | circleY = r * np.sin(thetas) 136 | top_vertices = np.vstack((circleX, circleY, circleY * 0 + h / 2)).T 137 | bottom_vertices = np.vstack((circleX, circleY, circleY * 0 - h / 2)).T 138 | vertices = np.array([[0, 0, h / 2], 139 | [0, 0, -h / 2]]) 140 | vertices = np.vstack((vertices, top_vertices, bottom_vertices)) 141 | vertices += center 142 | 143 | # Create faces 144 | top_faces = [[0, 2 + i, 2 + ((i + 1) % precision)] for i in range(precision)] 145 | bottom_faces = [[1, 2 + precision + i, 2 + precision + ((i + 1) % precision)] for i in range(precision)] 146 | side_faces1 = [[2 + i, 2 + precision + i, 2 + precision + ((i + 1) % precision)] for i in range(precision)] 147 | side_faces2 = [[2 + precision + ((i + 1) % precision), 2 + i, 2 + ((i + 1) % precision)] for i in range(precision)] 148 | faces = np.array(top_faces + bottom_faces + side_faces1 + side_faces2, dtype=np.int32) 149 | 150 | return vertices.astype(np.float32), faces 151 | -------------------------------------------------------------------------------- /JSENet_code/utils/metrics.py: -------------------------------------------------------------------------------- 1 | # 2 | # 3 | # 0=================================0 4 | # | Kernel Point Convolutions | 5 | # 0=================================0 6 | # 7 | # 8 | # ---------------------------------------------------------------------------------------------------------------------- 9 | # 10 | # Metric utility functions 11 | # 12 | # ---------------------------------------------------------------------------------------------------------------------- 13 | # 14 | # Hugues THOMAS - 11/06/2018 15 | # 16 | 17 | 18 | # ---------------------------------------------------------------------------------------------------------------------- 19 | # 20 | # Imports and global variables 21 | # \**********************************/ 22 | # 23 | 24 | 25 | # Basic libs 26 | import numpy as np 27 | 28 | 29 | # ---------------------------------------------------------------------------------------------------------------------- 30 | # 31 | # Utilities 32 | # \***************/ 33 | # 34 | 35 | 36 | def metrics(confusions, ignore_unclassified=False): 37 | """ 38 | Computes different metrics from confusion matrices. 39 | :param confusions: ([..., n_c, n_c] np.int32). Can be any dimension, the confusion matrices should be described by 40 | the last axes. n_c = number of classes 41 | :param ignore_unclassified: (bool). True if the the first class should be ignored in the results 42 | :return: ([..., n_c] np.float32) precision, recall, F1 score, IoU score 43 | """ 44 | 45 | # If the first class (often "unclassified") should be ignored, erase it from the confusion. 46 | if (ignore_unclassified): 47 | confusions[..., 0, :] = 0 48 | confusions[..., :, 0] = 0 49 | 50 | # Compute TP, FP, FN. This assume that the second to last axis counts the truths (like the first axis of a 51 | # confusion matrix), and that the last axis counts the predictions (like the second axis of a confusion matrix) 52 | TP = np.diagonal(confusions, axis1=-2, axis2=-1) 53 | TP_plus_FP = np.sum(confusions, axis=-1) 54 | TP_plus_FN = np.sum(confusions, axis=-2) 55 | 56 | # Compute precision and recall. This assume that the second to last axis counts the truths (like the first axis of 57 | # a confusion matrix), and that the last axis counts the predictions (like the second axis of a confusion matrix) 58 | PRE = TP / (TP_plus_FN + 1e-6) 59 | REC = TP / (TP_plus_FP + 1e-6) 60 | 61 | # Compute Accuracy 62 | ACC = np.sum(TP, axis=-1) / (np.sum(confusions, axis=(-2, -1)) + 1e-6) 63 | 64 | # Compute F1 score 65 | F1 = 2 * TP / (TP_plus_FP + TP_plus_FN + 1e-6) 66 | 67 | # Compute IoU 68 | IoU = F1 / (2 - F1) 69 | 70 | return PRE, REC, F1, IoU, ACC 71 | 72 | 73 | def smooth_metrics(confusions, smooth_n=0, ignore_unclassified=False): 74 | """ 75 | Computes different metrics from confusion matrices. Smoothed over a number of epochs. 76 | :param confusions: ([..., n_c, n_c] np.int32). Can be any dimension, the confusion matrices should be described by 77 | the last axes. n_c = number of classes 78 | :param smooth_n: (int). smooth extent 79 | :param ignore_unclassified: (bool). True if the the first class should be ignored in the results 80 | :return: ([..., n_c] np.float32) precision, recall, F1 score, IoU score 81 | """ 82 | 83 | # If the first class (often "unclassified") should be ignored, erase it from the confusion. 84 | if ignore_unclassified: 85 | confusions[..., 0, :] = 0 86 | confusions[..., :, 0] = 0 87 | 88 | # Sum successive confusions for smoothing 89 | smoothed_confusions = confusions.copy() 90 | if confusions.ndim > 2 and smooth_n > 0: 91 | for epoch in range(confusions.shape[-3]): 92 | i0 = max(epoch - smooth_n, 0) 93 | i1 = min(epoch + smooth_n + 1, confusions.shape[-3]) 94 | smoothed_confusions[..., epoch, :, :] = np.sum(confusions[..., i0:i1, :, :], axis=-3) 95 | 96 | # Compute TP, FP, FN. This assume that the second to last axis counts the truths (like the first axis of a 97 | # confusion matrix), and that the last axis counts the predictions (like the second axis of a confusion matrix) 98 | TP = np.diagonal(smoothed_confusions, axis1=-2, axis2=-1) 99 | TP_plus_FP = np.sum(smoothed_confusions, axis=-2) 100 | TP_plus_FN = np.sum(smoothed_confusions, axis=-1) 101 | 102 | # Compute precision and recall. This assume that the second to last axis counts the truths (like the first axis of 103 | # a confusion matrix), and that the last axis counts the predictions (like the second axis of a confusion matrix) 104 | PRE = TP / (TP_plus_FN + 1e-6) 105 | REC = TP / (TP_plus_FP + 1e-6) 106 | 107 | # Compute Accuracy 108 | ACC = np.sum(TP, axis=-1) / (np.sum(smoothed_confusions, axis=(-2, -1)) + 1e-6) 109 | 110 | # Compute F1 score 111 | F1 = 2 * TP / (TP_plus_FP + TP_plus_FN + 1e-6) 112 | 113 | # Compute IoU 114 | IoU = F1 / (2 - F1) 115 | 116 | return PRE, REC, F1, IoU, ACC 117 | 118 | 119 | def IoU_from_confusions(confusions): 120 | """ 121 | Computes IoU from confusion matrices. 122 | :param confusions: ([..., n_c, n_c] np.int32). Can be any dimension, the confusion matrices should be described by 123 | the last axes. n_c = number of classes 124 | :param ignore_unclassified: (bool). True if the the first class should be ignored in the results 125 | :return: ([..., n_c] np.float32) IoU score 126 | """ 127 | 128 | # Compute TP, FP, FN. This assume that the second to last axis counts the truths (like the first axis of a 129 | # confusion matrix), and that the last axis counts the predictions (like the second axis of a confusion matrix) 130 | TP = np.diagonal(confusions, axis1=-2, axis2=-1) 131 | TP_plus_FN = np.sum(confusions, axis=-1) 132 | TP_plus_FP = np.sum(confusions, axis=-2) 133 | 134 | # Compute IoU 135 | IoU = TP / (TP_plus_FP + TP_plus_FN - TP + 1e-6) 136 | 137 | # Compute mIoU with only the actual classes 138 | mask = TP_plus_FN < 1e-3 139 | counts = np.sum(1 - mask, axis=-1, keepdims=True) 140 | mIoU = np.sum(IoU, axis=-1, keepdims=True) / (counts + 1e-6) 141 | 142 | # If class is absent, place mIoU in place of 0 IoU to get the actual mean later 143 | IoU += mask * mIoU 144 | 145 | return IoU 146 | 147 | -------------------------------------------------------------------------------- /JSENet_code/utils/ply.py: -------------------------------------------------------------------------------- 1 | # 2 | # 3 | # 0===============================0 4 | # | PLY files reader/writer | 5 | # 0===============================0 6 | # 7 | # 8 | # ---------------------------------------------------------------------------------------------------------------------- 9 | # 10 | # function to read/write .ply files 11 | # 12 | # ---------------------------------------------------------------------------------------------------------------------- 13 | # 14 | # Hugues THOMAS - 10/02/2017 15 | # 16 | 17 | 18 | # ---------------------------------------------------------------------------------------------------------------------- 19 | # 20 | # Imports and global variables 21 | # \**********************************/ 22 | # 23 | 24 | 25 | # Basic libs 26 | import numpy as np 27 | import sys 28 | 29 | 30 | # Define PLY types 31 | ply_dtypes = dict([ 32 | (b'int8', 'i1'), 33 | (b'char', 'i1'), 34 | (b'uint8', 'u1'), 35 | (b'uchar', 'u1'), 36 | (b'int16', 'i2'), 37 | (b'short', 'i2'), 38 | (b'uint16', 'u2'), 39 | (b'ushort', 'u2'), 40 | (b'int32', 'i4'), 41 | (b'int', 'i4'), 42 | (b'uint32', 'u4'), 43 | (b'uint', 'u4'), 44 | (b'float32', 'f4'), 45 | (b'float', 'f4'), 46 | (b'float64', 'f8'), 47 | (b'double', 'f8') 48 | ]) 49 | 50 | # Numpy reader format 51 | valid_formats = {'ascii': '', 'binary_big_endian': '>', 52 | 'binary_little_endian': '<'} 53 | 54 | 55 | # ---------------------------------------------------------------------------------------------------------------------- 56 | # 57 | # Functions 58 | # \***************/ 59 | # 60 | 61 | 62 | def parse_header(plyfile, ext): 63 | # Variables 64 | line = [] 65 | properties = [] 66 | num_points = None 67 | 68 | while b'end_header' not in line and line != b'': 69 | line = plyfile.readline() 70 | 71 | if b'element' in line: 72 | line = line.split() 73 | num_points = int(line[2]) 74 | 75 | elif b'property' in line: 76 | line = line.split() 77 | properties.append((line[2].decode(), ext + ply_dtypes[line[1]])) 78 | 79 | return num_points, properties 80 | 81 | 82 | def parse_mesh_header(plyfile, ext): 83 | # Variables 84 | line = [] 85 | vertex_properties = [] 86 | num_points = None 87 | num_faces = None 88 | current_element = None 89 | 90 | 91 | while b'end_header' not in line and line != b'': 92 | line = plyfile.readline() 93 | 94 | # Find point element 95 | if b'element vertex' in line: 96 | current_element = 'vertex' 97 | line = line.split() 98 | num_points = int(line[2]) 99 | 100 | elif b'element face' in line: 101 | current_element = 'face' 102 | line = line.split() 103 | num_faces = int(line[2]) 104 | 105 | elif b'property' in line: 106 | if current_element == 'vertex': 107 | line = line.split() 108 | vertex_properties.append((line[2].decode(), ext + ply_dtypes[line[1]])) 109 | elif current_element == 'vertex': 110 | if not line.startswith('property list uchar int'): 111 | raise ValueError('Unsupported faces property : ' + line) 112 | 113 | return num_points, num_faces, vertex_properties 114 | 115 | 116 | def read_ply(filename, triangular_mesh=False): 117 | """ 118 | Read ".ply" files 119 | 120 | Parameters 121 | ---------- 122 | filename : string 123 | the name of the file to read. 124 | 125 | Returns 126 | ------- 127 | result : array 128 | data stored in the file 129 | 130 | Examples 131 | -------- 132 | Store data in file 133 | 134 | >>> points = np.random.rand(5, 3) 135 | >>> values = np.random.randint(2, size=10) 136 | >>> write_ply('example.ply', [points, values], ['x', 'y', 'z', 'values']) 137 | 138 | Read the file 139 | 140 | >>> data = read_ply('example.ply') 141 | >>> values = data['values'] 142 | array([0, 0, 1, 1, 0]) 143 | 144 | >>> points = np.vstack((data['x'], data['y'], data['z'])).T 145 | array([[ 0.466 0.595 0.324] 146 | [ 0.538 0.407 0.654] 147 | [ 0.850 0.018 0.988] 148 | [ 0.395 0.394 0.363] 149 | [ 0.873 0.996 0.092]]) 150 | 151 | """ 152 | 153 | with open(filename, 'rb') as plyfile: 154 | 155 | 156 | # Check if the file start with ply 157 | if b'ply' not in plyfile.readline(): 158 | raise ValueError('The file does not start whith the word ply') 159 | 160 | # get binary_little/big or ascii 161 | fmt = plyfile.readline().split()[1].decode() 162 | if fmt == "ascii": 163 | raise ValueError('The file is not binary') 164 | 165 | # get extension for building the numpy dtypes 166 | ext = valid_formats[fmt] 167 | 168 | # PointCloud reader vs mesh reader 169 | if triangular_mesh: 170 | 171 | # Parse header 172 | num_points, num_faces, properties = parse_mesh_header(plyfile, ext) 173 | 174 | # Get point data 175 | vertex_data = np.fromfile(plyfile, dtype=properties, count=num_points) 176 | 177 | # Get face data 178 | face_properties = [('k', ext + 'u1'), 179 | ('v1', ext + 'i4'), 180 | ('v2', ext + 'i4'), 181 | ('v3', ext + 'i4')] 182 | faces_data = np.fromfile(plyfile, dtype=face_properties, count=num_faces) 183 | 184 | # Return vertex data and concatenated faces 185 | faces = np.vstack((faces_data['v1'], faces_data['v2'], faces_data['v3'])).T 186 | data = [vertex_data, faces] 187 | 188 | else: 189 | 190 | # Parse header 191 | num_points, properties = parse_header(plyfile, ext) 192 | 193 | # Get data 194 | data = np.fromfile(plyfile, dtype=properties, count=num_points) 195 | 196 | return data 197 | 198 | 199 | def header_properties(field_list, field_names): 200 | 201 | # List of lines to write 202 | lines = [] 203 | 204 | # First line describing element vertex 205 | lines.append('element vertex %d' % field_list[0].shape[0]) 206 | 207 | # Properties lines 208 | i = 0 209 | for fields in field_list: 210 | for field in fields.T: 211 | lines.append('property %s %s' % (field.dtype.name, field_names[i])) 212 | i += 1 213 | 214 | return lines 215 | 216 | 217 | def write_ply(filename, field_list, field_names, triangular_faces=None): 218 | """ 219 | Write ".ply" files 220 | 221 | Parameters 222 | ---------- 223 | filename : string 224 | the name of the file to which the data is saved. A '.ply' extension will be appended to the 225 | file name if it does no already have one. 226 | 227 | field_list : list, tuple, numpy array 228 | the fields to be saved in the ply file. Either a numpy array, a list of numpy arrays or a 229 | tuple of numpy arrays. Each 1D numpy array and each column of 2D numpy arrays are considered 230 | as one field. 231 | 232 | field_names : list 233 | the name of each fields as a list of strings. Has to be the same length as the number of 234 | fields. 235 | 236 | Examples 237 | -------- 238 | >>> points = np.random.rand(10, 3) 239 | >>> write_ply('example1.ply', points, ['x', 'y', 'z']) 240 | 241 | >>> values = np.random.randint(2, size=10) 242 | >>> write_ply('example2.ply', [points, values], ['x', 'y', 'z', 'values']) 243 | 244 | >>> colors = np.random.randint(255, size=(10,3), dtype=np.uint8) 245 | >>> field_names = ['x', 'y', 'z', 'red', 'green', 'blue', values'] 246 | >>> write_ply('example3.ply', [points, colors, values], field_names) 247 | 248 | """ 249 | 250 | # Format list input to the right form 251 | field_list = list(field_list) if (type(field_list) == list or type(field_list) == tuple) else list((field_list,)) 252 | for i, field in enumerate(field_list): 253 | if field.ndim < 2: 254 | field_list[i] = field.reshape(-1, 1) 255 | if field.ndim > 2: 256 | print('fields have more than 2 dimensions') 257 | return False 258 | 259 | # check all fields have the same number of data 260 | n_points = [field.shape[0] for field in field_list] 261 | if not np.all(np.equal(n_points, n_points[0])): 262 | print('wrong field dimensions') 263 | return False 264 | 265 | # Check if field_names and field_list have same nb of column 266 | n_fields = np.sum([field.shape[1] for field in field_list]) 267 | if (n_fields != len(field_names)): 268 | print('wrong number of field names') 269 | return False 270 | 271 | # Add extension if not there 272 | if not filename.endswith('.ply'): 273 | filename += '.ply' 274 | 275 | # open in text mode to write the header 276 | with open(filename, 'w') as plyfile: 277 | 278 | # First magical word 279 | header = ['ply'] 280 | 281 | # Encoding format 282 | header.append('format binary_' + sys.byteorder + '_endian 1.0') 283 | 284 | # Points properties description 285 | header.extend(header_properties(field_list, field_names)) 286 | 287 | # Add faces if needded 288 | if triangular_faces is not None: 289 | header.append('element face {:d}'.format(triangular_faces.shape[0])) 290 | header.append('property list uchar int vertex_indices') 291 | 292 | # End of header 293 | header.append('end_header') 294 | 295 | # Write all lines 296 | for line in header: 297 | plyfile.write("%s\n" % line) 298 | 299 | # open in binary/append to use tofile 300 | with open(filename, 'ab') as plyfile: 301 | 302 | # Create a structured array 303 | i = 0 304 | type_list = [] 305 | for fields in field_list: 306 | for field in fields.T: 307 | type_list += [(field_names[i], field.dtype.str)] 308 | i += 1 309 | data = np.empty(field_list[0].shape[0], dtype=type_list) 310 | i = 0 311 | for fields in field_list: 312 | for field in fields.T: 313 | data[field_names[i]] = field 314 | i += 1 315 | 316 | data.tofile(plyfile) 317 | 318 | if triangular_faces is not None: 319 | triangular_faces = triangular_faces.astype(np.int32) 320 | type_list = [('k', 'uint8')] + [(str(ind), 'int32') for ind in range(3)] 321 | data = np.empty(triangular_faces.shape[0], dtype=type_list) 322 | data['k'] = np.full((triangular_faces.shape[0],), 3, dtype=np.uint8) 323 | data['0'] = triangular_faces[:, 0] 324 | data['1'] = triangular_faces[:, 1] 325 | data['2'] = triangular_faces[:, 2] 326 | data.tofile(plyfile) 327 | 328 | return True 329 | 330 | 331 | def describe_element(name, df): 332 | """ Takes the columns of the dataframe and builds a ply-like description 333 | 334 | Parameters 335 | ---------- 336 | name: str 337 | df: pandas DataFrame 338 | 339 | Returns 340 | ------- 341 | element: list[str] 342 | """ 343 | property_formats = {'f': 'float', 'u': 'uchar', 'i': 'int'} 344 | element = ['element ' + name + ' ' + str(len(df))] 345 | 346 | if name == 'face': 347 | element.append("property list uchar int points_indices") 348 | 349 | else: 350 | for i in range(len(df.columns)): 351 | # get first letter of dtype to infer format 352 | f = property_formats[str(df.dtypes[i])[0]] 353 | element.append('property ' + f + ' ' + df.columns.values[i]) 354 | 355 | return element 356 | 357 | -------------------------------------------------------------------------------- /JSENet_code/utils/trainer.py: -------------------------------------------------------------------------------- 1 | # 2 | # 3 | # 0=================================0 4 | # | Kernel Point Convolutions | 5 | # 0=================================0 6 | # 7 | # 8 | # ---------------------------------------------------------------------------------------------------------------------- 9 | # 10 | # Class handling the training of any model 11 | # 12 | # ---------------------------------------------------------------------------------------------------------------------- 13 | # 14 | # Hugues THOMAS - 11/06/2018 15 | # 16 | 17 | 18 | # ---------------------------------------------------------------------------------------------------------------------- 19 | # 20 | # Imports and global variables 21 | # \**********************************/ 22 | # 23 | 24 | 25 | # Basic libs 26 | import tensorflow as tf 27 | import numpy as np 28 | import os 29 | from os import makedirs, remove 30 | from os.path import exists, join 31 | import time 32 | import psutil 33 | import sys 34 | 35 | # PLY reader 36 | from utils.ply import read_ply, write_ply 37 | 38 | # ---------------------------------------------------------------------------------------------------------------------- 39 | # 40 | # Trainer Class 41 | # \*******************/ 42 | # 43 | 44 | 45 | class ModelTrainer: 46 | 47 | # Initiation methods 48 | # ------------------------------------------------------------------------------------------------------------------ 49 | 50 | def __init__(self, model, restore_snap=None): 51 | 52 | # Add training ops 53 | self.add_train_ops(model) 54 | 55 | # Tensorflow Saver definition 56 | my_vars = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope='KernelPointNetwork') 57 | self.saver = tf.train.Saver(my_vars, max_to_keep=100) 58 | 59 | # Create a session for running Ops on the Graph. 60 | on_CPU = False 61 | if on_CPU: 62 | cProto = tf.ConfigProto(device_count={'GPU': 0}) 63 | else: 64 | cProto = tf.ConfigProto() 65 | cProto.gpu_options.allow_growth = True 66 | self.sess = tf.Session(config=cProto) 67 | 68 | # Init variables 69 | self.sess.run(tf.global_variables_initializer()) 70 | 71 | # Name of the snapshot to restore to (None if you want to start from beginning) 72 | if (restore_snap is not None): 73 | self.saver.restore(self.sess, restore_snap) 74 | print("Model restored from " + restore_snap) 75 | 76 | def add_train_ops(self, model): 77 | """ 78 | Add training ops on top of the model 79 | """ 80 | 81 | ############## 82 | # Training ops 83 | ############## 84 | 85 | with tf.variable_scope('optimizer'): 86 | 87 | # Learning rate as a Variable so we can modify it 88 | self.learning_rate = tf.Variable(model.config.learning_rate, trainable=False, name='learning_rate') 89 | 90 | # Create the gradient descent optimizer with the given learning rate. 91 | optimizer = tf.train.MomentumOptimizer(self.learning_rate, model.config.momentum) 92 | 93 | # Training step op 94 | gvs = optimizer.compute_gradients(model.loss) 95 | 96 | if model.config.grad_clip_norm > 0: 97 | 98 | # Get gradient for deformable convolutions and scale them 99 | scaled_gvs = [] 100 | for grad, var in gvs: 101 | if 'offset_conv' in var.name: 102 | scaled_gvs.append((0.1 * grad, var)) 103 | if 'offset_mlp' in var.name: 104 | scaled_gvs.append((0.1 * grad, var)) 105 | else: 106 | scaled_gvs.append((grad, var)) 107 | 108 | # Clipping each gradient independantly 109 | capped_gvs = [(tf.clip_by_norm(grad, model.config.grad_clip_norm), var) for grad, var in scaled_gvs] 110 | 111 | extra_update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS) 112 | with tf.control_dependencies(extra_update_ops): 113 | self.train_op = optimizer.apply_gradients(capped_gvs) 114 | 115 | else: 116 | extra_update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS) 117 | with tf.control_dependencies(extra_update_ops): 118 | self.train_op = optimizer.apply_gradients(gvs) 119 | 120 | return 121 | 122 | # Training main method 123 | # ------------------------------------------------------------------------------------------------------------------ 124 | 125 | def train(self, model, dataset, debug_NaN=False): 126 | """ 127 | Train the model on a particular dataset. 128 | """ 129 | 130 | if debug_NaN: 131 | # Add checking ops 132 | self.check_op = tf.add_check_numerics_ops() 133 | 134 | # Parameters log file 135 | if model.config.saving: 136 | model.parameters_log() 137 | 138 | # Train loop variables 139 | t0 = time.time() 140 | self.training_step = 0 141 | self.training_epoch = 0 142 | mean_dt = np.zeros(2) 143 | last_display = t0 144 | epoch_n = 1 145 | mean_epoch_n = 0 146 | 147 | # Initialise iterator with train data 148 | self.sess.run(dataset.train_init_op) 149 | 150 | #-------------------------------------------------------------------------------------------------------------------------------------- 151 | # Training dual network 152 | if model.config.saving: 153 | # Training log file 154 | with open(join(model.saving_path, 'training.txt'), "w") as file: 155 | file.write('Steps loss_edge_coarse loss_edge_s0 loss_edge_s1 loss_region_coarse loss_region_s0 loss_region_s1 loss_efr_s0 loss_efr_s1 loss_side reg_loss time memory\n') 156 | 157 | 158 | # Killing file (simply delete this file when you want to stop the training) 159 | if not exists(join(model.saving_path, 'running_PID.txt')): 160 | with open(join(model.saving_path, 'running_PID.txt'), "w") as file: 161 | file.write('Delete this file to stop training') 162 | 163 | 164 | # Start loop 165 | while self.training_epoch < model.config.max_epoch: 166 | 167 | try: 168 | # Run one step of the model. 169 | t = [time.time()] 170 | ops = [self.train_op, 171 | model.loss_edge_coarse, 172 | model.loss_edge_s0, 173 | model.loss_edge_s1, 174 | model.loss_region_coarse, 175 | model.loss_region_s0, 176 | model.loss_region_s1, 177 | model.loss_efr_s0, 178 | model.loss_efr_s1, 179 | model.loss_side, 180 | model.regularization_loss 181 | ] 182 | 183 | 184 | # Run normal 185 | _, L_edge_coarse, L_edge_s0, L_edge_s1, L_region_coarse, L_region_s0, L_region_s1, L_efr_s0, L_efr_s1, L_side, L_reg = self.sess.run(ops, {model.dropout_prob: 0.5}) 186 | 187 | t += [time.time()] 188 | 189 | # Average timing 190 | mean_dt = 0.95 * mean_dt + 0.05 * (np.array(t[1:]) - np.array(t[:-1])) 191 | 192 | # Console display (only one per second) 193 | if (t[-1] - last_display) > 1.0: 194 | last_display = t[-1] 195 | message = 'Step {:08d} L_edge_coarse={:5.3f} L_edge_s0={:5.3f} L_edge_s1={:5.3f} L_region_coarse={:5.3f} L_region_s0={:5.3f} L_region_s1={:5.3f} L_efr_s0={:5.3f} L_efr_s1={:5.3f} L_side={:5.3f} L_reg={:5.3f}' \ 196 | '---{:8.2f} ms/batch (Averaged)' 197 | print(message.format(self.training_step, 198 | L_edge_coarse, 199 | L_edge_s0, 200 | L_edge_s1, 201 | L_region_coarse, 202 | L_region_s0, 203 | L_region_s1, 204 | L_efr_s0, 205 | L_efr_s1, 206 | L_side, 207 | L_reg, 208 | 1000 * mean_dt[0], 209 | 1000 * mean_dt[1])) 210 | 211 | # Log file 212 | if model.config.saving: 213 | process = psutil.Process(os.getpid()) 214 | with open(join(model.saving_path, 'training.txt'), "a") as file: 215 | message = '{:d} {:.3f} {:.3f} {:.3f} {:.3f} {:.3f} {:.3f} {:.3f} {:.3f} {:.3f} {:.3f} {:.2f} {:.1f}\n' 216 | file.write(message.format(self.training_step, 217 | L_edge_coarse, 218 | L_edge_s0, 219 | L_edge_s1, 220 | L_region_coarse, 221 | L_region_s0, 222 | L_region_s1, 223 | L_efr_s0, 224 | L_efr_s1, 225 | L_side, 226 | L_reg, 227 | t[-1] - t0, 228 | process.memory_info().rss * 1e-6)) 229 | 230 | # Check kill signal (running_PID.txt deleted) 231 | if model.config.saving and not exists(join(model.saving_path, 'running_PID.txt')): 232 | break 233 | 234 | 235 | except tf.errors.OutOfRangeError: 236 | 237 | # End of train dataset, update average of epoch steps 238 | mean_epoch_n += (epoch_n - mean_epoch_n) / (self.training_epoch + 1) 239 | epoch_n = 0 240 | self.int = int(np.floor(mean_epoch_n)) 241 | model.config.epoch_steps = int(np.floor(mean_epoch_n)) 242 | if model.config.saving: 243 | model.parameters_log() 244 | 245 | # Snapshot 246 | if model.config.saving and (self.training_epoch + 1) % model.config.snapshot_gap == 0: 247 | 248 | # Tensorflow snapshot 249 | snapshot_directory = join(model.saving_path, 'snapshots') 250 | if not exists(snapshot_directory): 251 | makedirs(snapshot_directory) 252 | self.saver.save(self.sess, snapshot_directory + '/snap', global_step=self.training_step + 1) 253 | 254 | # Update learning rate 255 | if self.training_epoch in model.config.lr_decays: 256 | op = self.learning_rate.assign(tf.multiply(self.learning_rate, 257 | model.config.lr_decays[self.training_epoch])) 258 | self.sess.run(op) 259 | 260 | # Increment 261 | self.training_epoch += 1 262 | 263 | # Reset iterator on training data 264 | self.sess.run(dataset.train_init_op) 265 | 266 | except tf.errors.InvalidArgumentError as e: 267 | 268 | print('Caught a NaN error :') 269 | print(e.error_code) 270 | print(e.message) 271 | print(e.op) 272 | print(e.op.name) 273 | print([t.name for t in e.op.inputs]) 274 | print([t.name for t in e.op.outputs]) 275 | 276 | a = 1/0 277 | 278 | # Increment steps 279 | self.training_step += 1 280 | epoch_n += 1 281 | 282 | # Remove File for kill signal 283 | if exists(join(model.saving_path, 'running_PID.txt')): 284 | remove(join(model.saving_path, 'running_PID.txt')) 285 | self.sess.close() 286 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 HU Zeyu 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSENet: Joint Semantic Segmentation and Edge Detection Network for 3D Point Clouds 2 | ## Introduction 3 | Implementation of ECCV2020 paper - JSENet: Joint Semantic Segmentation and Edge Detection Network for 3D Point Clouds ([arXiv](https://arxiv.org/abs/2007.06888)). If you find our work useful in your research, please consider citing: 4 | 5 | ``` 6 | @inproceedings{hu2020jsenet, 7 | title={JSENet: Joint Semantic Segmentation and Edge Detection Network for 3D Point Clouds}, 8 | author={Hu, Zeyu and Zhen, Mingmin and Bai, Xuyang and Fu, Hongbo and Tai, Chiew-lan}, 9 | booktitle={Proceedings of the European Conference on Computer Vision (ECCV)}, 10 | pages={222--239}, 11 | year={2020} 12 | } 13 | ``` 14 | 15 | ## Installation 16 | 17 | This repository is modified from [KPConv](https://github.com/HuguesTHOMAS/KPConv/), please find the step-by-step installation guide in [INSTALL.md](https://github.com/HuguesTHOMAS/KPConv/blob/master/INSTALL.md). 18 | 19 | ## Experiments 20 | 21 | ### Data 22 | 23 | #### S3DIS 24 | 25 | S3DIS dataset can be downloaded here (4.8 GB). Download the file named `Stanford3dDataset_v1.2.zip`, uncompress the folder and move it to `Data/S3DIS`. 26 | 27 | We provide processed demo dataset for experiments on S3DIS fold-5. The demo dataset can be downloaded here (903 MB). Uncompress the folder and move it to `Data/S3DIS`. 28 | 29 | #### Scannet 30 | 31 | Scannet dataset can be find here. Follow the instructions and move downloaded files to `Data/Scannet`. 32 | 33 | ### Training 34 | For S3DIS dataset: 35 | 36 | python training_S3DIS.py 37 | 38 | 39 | For Scannet dataset: 40 | 41 | python training_Scannet.py 42 | 43 | If you are not using the processed demo dataset, the first run will take some time to process the raw data. The process can be easily accelerated using parallel computing methods like Pthreads. 44 | 45 | ### Testing 46 | 47 | In `test_model.py`, you will find detailed comments explaining how to choose which logged trained model you want to test. Follow them and then run the script : 48 | 49 | For semantic segmentation task: 50 | 51 | python test_model.py --task SS 52 | 53 | For semantic edge detection task: 54 | 55 | python test_model.py --task SED 56 | 57 | 58 | ### Pretrained models 59 | 60 | Pretrained models can be downloaded here (119 MB). Uncompress the files and move it to `results/`. 61 | 62 | 63 | ## Acknowledgment 64 | 65 | Our code is modified from [KPConv](https://github.com/HuguesTHOMAS/KPConv/). 66 | 67 | ## License 68 | Our code is released under MIT License (see LICENSE file for details). 69 | --------------------------------------------------------------------------------