├── .gitignore ├── README.md └── src ├── config.py ├── generate_synthesis_data.py ├── main.py ├── octree └── octree.py └── utils └── ply.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | report 3 | official 4 | data 5 | papers 6 | local 7 | *__pycache__ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PoissonSurfaceReconstruction 2 | -------------------------------------------------------------------------------- /src/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | #################### 4 | # file system misc # 5 | #################### 6 | 7 | root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 8 | output_dir = 'local' 9 | input_dir = 'data' 10 | 11 | inpdir = os.path.join(root, input_dir) 12 | outdir = os.path.join(root, output_dir) 13 | 14 | 15 | inpfile = 'bunny_normals' 16 | outsufx = 'last_test' 17 | sufx = '.ply' 18 | 19 | inppath = os.path.join(inpdir, inpfile + sufx) 20 | outpath = os.path.join(outdir, '_'.join([inpfile, outsufx]) + sufx) 21 | 22 | ############################# 23 | # algorithm hyperparameters # 24 | ############################# 25 | 26 | octdepth = 3 27 | divtempt = 1. 28 | 29 | def test(): 30 | print(os.path.dirname(os.path.abspath(__file__))) 31 | 32 | if __name__ == '__main__': 33 | test() 34 | -------------------------------------------------------------------------------- /src/generate_synthesis_data.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | import numpy as np 3 | import argparse 4 | 5 | import config 6 | from utils.ply import write_ply 7 | 8 | def generate_cube(side, sample_per_side, out): 9 | 10 | sps = sample_per_side 11 | arr = np.arange(sample_per_side**3) 12 | x = arr % sps 13 | y = arr // sps % sps 14 | z = arr // sps // sps % sps 15 | 16 | coords = np.vstack([x,y,z]).T 17 | is_boundary = np.logical_or.reduce(np.logical_or(coords==0, coords==sps-1), axis=-1) 18 | coords = coords[is_boundary].astype(np.float32) 19 | 20 | write_ply(out, (coords,), ['x', 'y', 'z']) 21 | 22 | 23 | if __name__ == '__main__': 24 | 25 | parser = argparse.ArgumentParser() 26 | parser.add_argument("--side", default=1., type=float, help="size of the cube in absolute value") 27 | parser.add_argument("--sps", default=51, type=int, help="number of points per side") 28 | parser.add_argument("--out", default=os.path.join(config.outdir, 'cube.ply'), type=str, help="path for output file") 29 | args = parser.parse_args() 30 | 31 | generate_cube(side=args.side, sample_per_side=args.sps, out='_'.join([args.out, str(args.sps)])) 32 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | 4 | import config 5 | from octree.octree import poisson_surface_reconstruction 6 | from utils.ply import read_ply, write_ply 7 | 8 | if __name__ == '__main__': 9 | 10 | data = read_ply(config.inppath) 11 | points = np.vstack((data['x'], data['y'], data['z'])).T 12 | normals = np.vstack((data['nx'], data['ny'], data['nz'])).T 13 | 14 | print('num of points',points.shape[0]) 15 | node_vals = poisson_surface_reconstruction( 16 | config.octdepth, 17 | config.divtempt)(points, normals) 18 | 19 | write_ply(config.outpath, list(node_vals.values()), list(node_vals.keys())) 20 | -------------------------------------------------------------------------------- /src/octree/octree.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from tqdm import tqdm 3 | 4 | # Base Functions 5 | def base(vec, func): 6 | return np.prod(func(vec)) 7 | 8 | def f1(vec): 9 | return np.logical_and(vec<+.5, vec>-.5) 10 | 11 | def f2(vec): 12 | return np.maximum(1.-np.abs(vec), 0.) 13 | 14 | def f3(x): 15 | abx = np.abs(x) 16 | b1 = abx < 0.5 17 | b2 = abx < 1.5 18 | vals = (1.5-abx)**2./2. * b2 * (1.-b1) \ 19 | + (-x**2. + 3./4.) * b1 20 | return vals 21 | 22 | def ddf3(x): 23 | abx = np.abs(x) 24 | b1 = abx < 0.5 25 | b2 = abx < 1.5 26 | return -2. * b1 + b2 * (1.-b1) 27 | 28 | octant_bits = np.array( 29 | [[(i&(1<0 for j in range(3)] for i in range(8)], 30 | dtype=int) 31 | 32 | def test(): 33 | 34 | # base function test 35 | import matplotlib.pyplot as plt 36 | l, h = intv = (-2, +2) 37 | prec = 1000 38 | x = np.linspace(l,h,prec*(h-l)+1) 39 | plt.plot(x,f1(x),'b',label='0th-order spline') 40 | plt.plot(x,f2(x),'g',label='1st-order spline') 41 | plt.plot(x,f3(x),'r',label='2nd-order spline') 42 | plt.legend() 43 | plt.show() 44 | 45 | def poisson_surface_reconstruction(depth, tempt): 46 | 47 | num_voxels = 2**depth 48 | 49 | def reconstructor(points, normals): 50 | 51 | def init_smoothing_filter(): 52 | 53 | def smoothing_filter(scaler): 54 | def scaled_filter(x): 55 | return np.prod(f3(scaler(x))) 56 | def dd_scaled_filter(x): 57 | return np.prod(ddf3(scaler(x))) 58 | return scaled_filter, dd_scaled_filter 59 | 60 | radius = 1.5 61 | 62 | return smoothing_filter, radius 63 | 64 | filter, radius = init_smoothing_filter() 65 | 66 | def build_octree(): 67 | 68 | # adjust the bounding box 69 | lows = np.amin(points, axis=0) 70 | highs = np.amax(points, axis=0) 71 | size = np.amax(highs-lows) * (1.+3./num_voxels) 72 | voxel_size = size / num_voxels 73 | means = (lows + highs)/2. 74 | lows = means - size/2. 75 | highs = means + size/2. 76 | print('lows',lows) 77 | print('highs',highs) 78 | print('voxel size',voxel_size) 79 | 80 | nodes = list() 81 | index_to_leaf = dict() 82 | 83 | def create_node( 84 | parent=dict( 85 | depth=-1, 86 | width=2.*size, 87 | center=means), 88 | octant=np.array([.5]*3)): 89 | pcenter = np.array(parent['center']) 90 | node = dict( 91 | depth=parent['depth']+1, 92 | width=.5*parent['width'], 93 | center=tuple(pcenter+(2*octant-1)*.25*parent['width']), 94 | is_lead=False) 95 | if node['depth'] >= depth: 96 | node['normal'] = np.zeros(3) 97 | node['is_leaf'] = True 98 | nodes.append(node) 99 | return node 100 | 101 | octree = create_node() 102 | 103 | def min_max_normalise(point): 104 | return np.array((point - lows) / voxel_size) 105 | 106 | def get_closest_vertex(relative_coord): 107 | return np.round(relative_coord).astype(int) 108 | 109 | def init_node(node): 110 | nodes.remove(node) 111 | for octant_bit in octant_bits: 112 | node[tuple(octant_bit)] = create_node(node, octant_bit) 113 | 114 | def insert_leaf(index): 115 | octants = list() 116 | tmpid = index 117 | for i in range(depth): 118 | octants.append(tmpid % 2) 119 | tmpid = tmpid // 2 120 | current_node = octree 121 | for octant in reversed(octants): 122 | octant = tuple(octant) 123 | if octant not in current_node: 124 | init_node(current_node) 125 | current_node = current_node[octant] 126 | current_node['index'] = index 127 | index_to_leaf[tuple(index)] = current_node 128 | 129 | def get_surroundings(vertex): 130 | indices = np.expand_dims(vertex, axis=0)+octant_bits-1 131 | surroundings = list() 132 | for index in indices: 133 | if tuple(index) not in index_to_leaf: 134 | insert_leaf(index) 135 | surroundings.append(index_to_leaf[tuple(index)]) 136 | return surroundings 137 | 138 | def get_trilinears(relative_coord, closest_vertex): 139 | delta = np.expand_dims(relative_coord - closest_vertex, axis=0) 140 | trilinears = np.prod( 141 | octant_bits * (.5-delta) + (1-octant_bits) * (.5+delta), 142 | axis=-1) 143 | return trilinears 144 | 145 | def get_surroundings_and_trilinears(location): 146 | relative_coord = min_max_normalise(point) 147 | closest_vertex = get_closest_vertex(relative_coord) 148 | surroundings = get_surroundings(closest_vertex) 149 | trilinears = get_trilinears(relative_coord, closest_vertex) 150 | return surroundings, trilinears 151 | 152 | def add_point(point, normal): 153 | for node, weight in zip(*get_surroundings_and_trilinears(point)): 154 | node['normal'] += weight * normal 155 | 156 | for point, normal in zip(points, normals): 157 | add_point(point, normal) 158 | 159 | true_radius = radius / num_voxels * size 160 | def aux_range_search(node, location): 161 | result = list() 162 | dist = np.amax(np.abs(location - node['center'])) 163 | if node['depth']==depth: 164 | if dist < true_radius: 165 | result.append(node) 166 | elif dist < true_radius + node['width']: 167 | for octant_bit in octant_bits: 168 | octant_bit = tuple(octant_bit) 169 | if octant_bit in node: 170 | result.extend(aux_range_search(node[octant_bit], location)) 171 | return result 172 | def range_search(location): 173 | location = np.array(location) 174 | return aux_range_search(octree, location) 175 | 176 | fields = ['center', 'gradient', 'divergence', 'v', 'chi'] 177 | fields_transform = { 178 | 'center':['x', 'y', 'z'], 179 | 'gradient':['nx', 'ny', 'nz'] 180 | } 181 | def octree_fields(): 182 | attribs = list(set(nodes[0].keys()).intersection(fields)) 183 | values = dict(zip(attribs, [[] for a in attribs])) 184 | for node in nodes: 185 | for attrib in attribs: 186 | values[attrib].append(node[attrib]) 187 | for attrib in attribs: 188 | values[attrib] = np.array(values[attrib]) 189 | for k in fields_transform: 190 | for i, v in enumerate(fields_transform[k]): 191 | values[v] = values[k][:,i] 192 | del values[k] 193 | return values 194 | 195 | def real_to_voxel_scaling(x): 196 | return x/voxel_size 197 | 198 | return octree, nodes, real_to_voxel_scaling, range_search, get_surroundings_and_trilinears, octree_fields 199 | 200 | octree, nodes, real_to_voxel_scaling, range_search, sur_and_tril, octree_fields = build_octree() 201 | filter, dd_filter = filter(real_to_voxel_scaling) 202 | 203 | def conv_like_op(): 204 | neighbours = dict() 205 | print('nodes len',len(nodes)) 206 | 207 | print('neighbour searching...') 208 | for i, node in tqdm(enumerate(nodes)): 209 | neighbours[i] = range_search(node['center']) 210 | 211 | def iterate_over_nodes(func): 212 | print('{} is being executed...'.format(func.name)) 213 | for i, node in tqdm(enumerate(nodes)): 214 | func(node, neighbours[i]) 215 | return iterate_over_nodes 216 | op_executor = conv_like_op() 217 | 218 | def compute_gradient(): 219 | def f(node, neighbours): 220 | node['gradient'] = np.zeros(3) 221 | for neighbour in neighbours: 222 | node['gradient'] += filter(np.array(neighbour['center'])-np.array(node['center'])) * neighbour['normal'] 223 | f.name = 'gradient computation' 224 | return f 225 | op_executor(compute_gradient()) 226 | 227 | def compute_divergence(): 228 | def f(node, neighbours): 229 | node['divergence'] = 0. 230 | for neighbour in neighbours: 231 | delta = np.array(neighbour['center'])-np.array(node['center']) 232 | if (delta < 1e-10).all(): 233 | continue 234 | dgrad = np.array(neighbour['gradient'])-np.array(node['gradient']) 235 | n_delta = np.sum(delta**2.)**.5 236 | node['divergence'] += filter(n_delta) * np.sum(delta * dgrad / n_delta) 237 | f.name = 'divergence computation' 238 | return f 239 | op_executor(compute_divergence()) 240 | 241 | def project_on_kernel_space(): 242 | def f(node, neighbours): 243 | node['v'] = 0. 244 | for neighbour in neighbours: 245 | node['v'] += filter(np.array(neighbour['center'])-np.array(node['center'])) * neighbour['divergence'] 246 | f.name = 'projection on finite basis' 247 | return f 248 | op_executor(project_on_kernel_space()) 249 | v = np.array([ node['v'] for node in nodes ]) 250 | 251 | def dxxf(x, ax): 252 | """ 253 | x is of dimension 1 254 | """ 255 | nonax = list(range(3)) 256 | nonax.remove(ax) 257 | return filter(x[nonax]) * dd_filter(x[ax]) 258 | def ddf_f_product(node1, node2): 259 | """ 260 | approximate dot product of functions 261 | """ 262 | c1 = np.array(node1['center']) 263 | c2 = np.array(node2['center']) 264 | return -6. * filter(c2-c1) + np.sum([dxxf(c2-c1, i) for i in range(3)]) 265 | from scipy import sparse as sp 266 | import scipy.sparse.linalg as splna 267 | for id, node in enumerate(nodes): 268 | node['id'] = id 269 | def get_laplacian_matrix(): 270 | # construct a sparse matrix 271 | data = list() 272 | rows = list() 273 | cols = list() 274 | 275 | def add_to_list(n1, n2): 276 | data.append(ddf_f_product(n1, n2)) 277 | rows.append(n1['id']) 278 | cols.append(n2['id']) 279 | 280 | def compute_dot_product(): 281 | def f(node, neighbours): 282 | add_to_list(node, node) 283 | for neighbour in neighbours: 284 | add_to_list(node, neighbour) 285 | f.name = 'laplacian computation' 286 | return f 287 | op_executor(compute_dot_product()) 288 | return sp.coo_matrix((data, (rows, cols)), shape=(len(nodes), len(nodes))).tocsr() 289 | 290 | L = get_laplacian_matrix() 291 | 292 | print('solving Poisson equation...') 293 | chis = splna.spsolve(L, v) 294 | for chi, node in zip(chis, nodes): 295 | node['chi'] = chi 296 | 297 | print('computing the mean of chi...') 298 | total_chi = 0. 299 | for point in tqdm(points): 300 | for sur, tril in zip(*sur_and_tril(point)): 301 | total_chi += tril * sur['chi'] 302 | mean_chi = total_chi / points.shape[0] 303 | 304 | print('mean_chi', mean_chi) 305 | volumes = np.zeros(shape=(2**depth,)*3) 306 | 307 | return octree_fields() 308 | 309 | return reconstructor 310 | 311 | if __name__ == '__main__': 312 | test() 313 | -------------------------------------------------------------------------------- /src/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', 'b1'), 36 | (b'uchar', 'u1'), 37 | (b'int16', 'i2'), 38 | (b'short', 'i2'), 39 | (b'uint16', 'u2'), 40 | (b'ushort', 'u2'), 41 | (b'int32', 'i4'), 42 | (b'int', 'i4'), 43 | (b'uint32', 'u4'), 44 | (b'uint', 'u4'), 45 | (b'float32', 'f4'), 46 | (b'float', 'f4'), 47 | (b'float64', 'f8'), 48 | (b'double', 'f8') 49 | ]) 50 | 51 | # Numpy reader format 52 | valid_formats = {'ascii': '', 'binary_big_endian': '>', 53 | 'binary_little_endian': '<'} 54 | 55 | 56 | #------------------------------------------------------------------------------------------ 57 | # 58 | # Functions 59 | # \***************/ 60 | # 61 | 62 | def parse_header(plyfile, ext): 63 | 64 | # Variables 65 | line = [] 66 | properties = [] 67 | num_points = None 68 | 69 | while b'end_header' not in line and line != b'': 70 | line = plyfile.readline() 71 | 72 | if b'element' in line: 73 | if b'vertex' in line: 74 | line = line.split() 75 | num_points = int(line[2]) 76 | 77 | elif b'property' in line and b'list' not in line: 78 | line = line.split() 79 | properties.append((line[2].decode(), ext + ply_dtypes[line[1]])) 80 | 81 | return num_points, properties 82 | 83 | 84 | 85 | 86 | def read_ply(filename): 87 | """ 88 | Read ".ply" files 89 | 90 | Parameters 91 | ---------- 92 | filename : string 93 | the name of the file to read. 94 | 95 | Returns 96 | ------- 97 | result : array 98 | data stored in the file 99 | 100 | Examples 101 | -------- 102 | Store data in file 103 | 104 | >>> points = np.random.rand(5, 3) 105 | >>> values = np.random.randint(2, size=10) 106 | >>> write_ply('example.ply', [points, values], ['x', 'y', 'z', 'values']) 107 | 108 | Read the file 109 | 110 | >>> data = read_ply('example.ply') 111 | >>> values = data['values'] 112 | array([0, 0, 1, 1, 0]) 113 | 114 | >>> points = np.vstack((data['x'], data['y'], data['z'])).T 115 | array([[ 0.466 0.595 0.324] 116 | [ 0.538 0.407 0.654] 117 | [ 0.850 0.018 0.988] 118 | [ 0.395 0.394 0.363] 119 | [ 0.873 0.996 0.092]]) 120 | 121 | """ 122 | 123 | with open(filename, 'rb') as plyfile: 124 | 125 | 126 | # Check if the file start with ply 127 | if b'ply' not in plyfile.readline(): 128 | raise ValueError('The file does not start whith the word ply') 129 | 130 | # get binary_little/big or ascii 131 | fmt = plyfile.readline().split()[1].decode() 132 | if fmt == "ascii": 133 | raise ValueError('The file is not binary') 134 | 135 | # get extension for building the numpy dtypes 136 | ext = valid_formats[fmt] 137 | 138 | # Parse header 139 | num_points, properties = parse_header(plyfile, ext) 140 | 141 | # Get data 142 | data = np.fromfile(plyfile, dtype=properties, count=num_points) 143 | 144 | 145 | return data 146 | 147 | 148 | 149 | 150 | def header_properties(field_list, field_names): 151 | 152 | # List of lines to write 153 | lines = [] 154 | 155 | # First line describing element vertex 156 | lines.append('element vertex %d' % field_list[0].shape[0]) 157 | 158 | # Properties lines 159 | i = 0 160 | for fields in field_list: 161 | for field in fields.T: 162 | lines.append('property %s %s' % (field.dtype.name, field_names[i])) 163 | i += 1 164 | 165 | return lines 166 | 167 | 168 | 169 | 170 | def write_ply(filename, field_list, field_names): 171 | """ 172 | Write ".ply" files 173 | 174 | Parameters 175 | ---------- 176 | filename : string 177 | the name of the file to which the data is saved. A '.ply' extension will be appended to the 178 | file name if it does no already have one. 179 | 180 | field_list : list, tuple, numpy array 181 | the fields to be saved in the ply file. Either a numpy array, a list of numpy arrays or a 182 | tuple of numpy arrays. Each 1D numpy array and each column of 2D numpy arrays are considered 183 | as one field. 184 | 185 | field_names : list 186 | the name of each fields as a list of strings. Has to be the same length as the number of 187 | fields. 188 | 189 | Examples 190 | -------- 191 | >>> points = np.random.rand(10, 3) 192 | >>> write_ply('example1.ply', points, ['x', 'y', 'z']) 193 | 194 | >>> values = np.random.randint(2, size=10) 195 | >>> write_ply('example2.ply', [points, values], ['x', 'y', 'z', 'values']) 196 | 197 | >>> colors = np.random.randint(255, size=(10,3), dtype=np.uint8) 198 | >>> field_names = ['x', 'y', 'z', 'red', 'green', 'blue', values'] 199 | >>> write_ply('example3.ply', [points, colors, values], field_names) 200 | 201 | """ 202 | 203 | # Format list input to the right form 204 | field_list = list(field_list) if (type(field_list) == list or type(field_list) == tuple) else list((field_list,)) 205 | for i, field in enumerate(field_list): 206 | if field is None: 207 | print('WRITE_PLY ERROR: a field is None') 208 | return False 209 | elif field.ndim > 2: 210 | print('WRITE_PLY ERROR: a field have more than 2 dimensions') 211 | return False 212 | elif field.ndim < 2: 213 | field_list[i] = field.reshape(-1, 1) 214 | 215 | # check all fields have the same number of data 216 | n_points = [field.shape[0] for field in field_list] 217 | if not np.all(np.equal(n_points, n_points[0])): 218 | print('wrong field dimensions') 219 | return False 220 | 221 | # Check if field_names and field_list have same nb of column 222 | n_fields = np.sum([field.shape[1] for field in field_list]) 223 | if (n_fields != len(field_names)): 224 | print('wrong number of field names') 225 | return False 226 | 227 | # Add extension if not there 228 | if not filename.endswith('.ply'): 229 | filename += '.ply' 230 | 231 | # open in text mode to write the header 232 | with open(filename, 'w') as plyfile: 233 | 234 | # First magical word 235 | header = ['ply'] 236 | 237 | # Encoding format 238 | header.append('format binary_' + sys.byteorder + '_endian 1.0') 239 | 240 | # Points properties description 241 | header.extend(header_properties(field_list, field_names)) 242 | 243 | # End of header 244 | header.append('end_header') 245 | 246 | # Write all lines 247 | for line in header: 248 | plyfile.write("%s\n" % line) 249 | 250 | 251 | # open in binary/append to use tofile 252 | with open(filename, 'ab') as plyfile: 253 | 254 | # Create a structured array 255 | i = 0 256 | type_list = [] 257 | for fields in field_list: 258 | for field in fields.T: 259 | type_list += [(field_names[i], field.dtype.str)] 260 | i += 1 261 | data = np.empty(field_list[0].shape[0], dtype=type_list) 262 | i = 0 263 | for fields in field_list: 264 | for field in fields.T: 265 | data[field_names[i]] = field 266 | i += 1 267 | 268 | data.tofile(plyfile) 269 | 270 | return True 271 | 272 | 273 | def describe_element(name, df): 274 | """ Takes the columns of the dataframe and builds a ply-like description 275 | 276 | Parameters 277 | ---------- 278 | name: str 279 | df: pandas DataFrame 280 | 281 | Returns 282 | ------- 283 | element: list[str] 284 | """ 285 | property_formats = {'f': 'float', 'u': 'uchar', 'i': 'int'} 286 | element = ['element ' + name + ' ' + str(len(df))] 287 | 288 | if name == 'face': 289 | element.append("property list uchar int points_indices") 290 | 291 | else: 292 | for i in range(len(df.columns)): 293 | # get first letter of dtype to infer format 294 | f = property_formats[str(df.dtypes[i])[0]] 295 | element.append('property ' + f + ' ' + df.columns.values[i]) 296 | 297 | return element 298 | --------------------------------------------------------------------------------