├── README.md ├── generate_example_tree.py ├── las.py ├── media ├── example_trees.png └── process.png ├── ply.py ├── requirements.txt └── treesim.py /README.md: -------------------------------------------------------------------------------- 1 | # Simple Synth Tree 2 | A python-based simulation model for generating synthetic tree point clouds. 3 | 4 | Source code associated with: 5 | 6 | M. Bryson, F. Wang and J. Allworth, "Using synthetic tree data in deep learning-based individual tree segmentation using LiDAR point clouds", Remote Sens. 2023, 15(9), 2380. 7 | 8 | [https://www.mdpi.com/2072-4292/15/9/2380](https://www.mdpi.com/2072-4292/15/9/2380) 9 | 10 | Requires: numpy, trimesh and laspy (for las file export). 11 | 12 | ![](media/example_trees.png) 13 | 14 | ## About 15 | "Simple Synth Tree" is a light-weight python library for generating simple synthetic point clouds of simulated trees. The simulation generates a randomised tree model composed of a central stem mesh and foliage/small branches, from which points are sampled from the surface of the mesh. Output point clouds are provided with per-point labels into one of two classes (class 0: foliage/small branches and class 1: tree stem). 16 | 17 | ![](media/process.png) 18 | 19 | Different tree structures can be generated by tuning the parameters of the model to control aspects such as tree height, canopy width and shape, branching etc. The simulator is designed to produce synthetic point clouds used to help train deep neural networks for LiDAR point cloud-based tree analysis tasks (see our Remote Sensing paper for more details and analysis of how using synthetic examples during training can be used to boost performance on real LiDAR point cloud processing tasks in forest remote sensing). 20 | 21 | ## Usage 22 | see "generate_example_tree.py": 23 | 24 | points = gen_simtree(Np=4096) 25 | export_points_ply('example001.ply', points) 26 | -------------------------------------------------------------------------------- /generate_example_tree.py: -------------------------------------------------------------------------------- 1 | """ 2 | generate_example_tree.py: generates an example synthetic tree point cloud and exports to 3 | a PLY file 4 | Author: Mitch Bryson 5 | """ 6 | 7 | from treesim import gen_simtree 8 | from ply import export_points_ply 9 | from las import export_points_las 10 | 11 | # Generate a single synthetic tree example and export as a PLY file 12 | points = gen_simtree(Np=4096) 13 | export_points_ply('example001.ply', points) 14 | 15 | # Generate a single synthetic tree example and export as a LAS file 16 | points = gen_simtree(Np=4096) 17 | export_points_las('example001.las', points) -------------------------------------------------------------------------------- /las.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import laspy 3 | 4 | def export_points_las(filename, points): 5 | """ 6 | Export points to a LAS file 7 | :param filename: output file name 8 | :param points: point cloud data 9 | """ 10 | # Create a new LAS data 11 | las = laspy.create(point_format=2, file_version='1.2') 12 | 13 | # Set the point records 14 | las.X = points[:, 0] 15 | las.Y = points[:, 1] 16 | las.Z = points[:, 2] 17 | las.classification = points[:, 3].astype(np.uint8) # Classification must be an unsigned 8-bit integer 18 | 19 | # Write the LAS file 20 | las.write(filename) -------------------------------------------------------------------------------- /media/example_trees.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchbryson/SimpleSynthTree/aa43f74960bfcfe26e224c387ffeaceafc50d051/media/example_trees.png -------------------------------------------------------------------------------- /media/process.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchbryson/SimpleSynthTree/aa43f74960bfcfe26e224c387ffeaceafc50d051/media/process.png -------------------------------------------------------------------------------- /ply.py: -------------------------------------------------------------------------------- 1 | """ 2 | ply.py: Functions for export points in PLY format 3 | Author: Mitch Bryson 4 | """ 5 | 6 | # export_points_ply: exports a point cloud in ASCII PLY format 7 | def export_points_ply(filepath, xyzl): 8 | 9 | f = open(filepath, "w"); 10 | f.write('ply\n') 11 | f.write('format ascii 1.0\n') 12 | f.write('element vertex %d\n'%(xyzl.shape[0])) 13 | f.write('property float x\n') 14 | f.write('property float y\n') 15 | f.write('property float z\n') 16 | f.write('property uchar diffuse_red\n') 17 | f.write('property uchar diffuse_green\n') 18 | f.write('property uchar diffuse_blue\n') 19 | f.write('end_header\n') 20 | c = 0 21 | for i in range(xyzl.shape[0]): 22 | if xyzl[i,3] == 0: # foliage point (green) 23 | (R,G,B) = (0,255,0) 24 | else: # stem point (red) 25 | (R,G,B) = (255,0,0) 26 | f.write('%.4f %.4f %.4f %d %d %d\n'%(xyzl[i,0],xyzl[i,1],xyzl[i,2],int(R),int(G),int(B))) 27 | f.close() 28 | 29 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | laspy 3 | trimesh 4 | -------------------------------------------------------------------------------- /treesim.py: -------------------------------------------------------------------------------- 1 | """ 2 | treesim.py: Functions for tree model simulation and sampling 3 | Author: Mitch Bryson 4 | 5 | Requires NumPy and Trimesh 6 | """ 7 | 8 | import os, sys 9 | from math import * 10 | import trimesh 11 | import numpy as np 12 | 13 | # get_spline_params: get spline parameters from points 14 | def get_spline_params(p0, p1, p2): 15 | A = np.array([ [2/(p1[0]-p0[0]), 1/(p1[0]-p0[0]), 0], \ 16 | [1/(p1[0]-p0[0]), 2*((1/(p1[0]-p0[0]))+(1/(p2[0]-p1[0]))), 1/(p2[0]-p1[0])], \ 17 | [0, 1/(p2[0]-p1[0]), 2/(p2[0]-p1[0])] ]) 18 | b = np.array([3*(p1[1]-p0[1])/pow(p1[0]-p0[0],2), \ 19 | 3*( (p1[1]-p0[1])/pow(p1[0]-p0[0],2) + (p2[1]-p1[1])/pow(p2[0]-p1[0],2) ), \ 20 | 3*(p2[1]-p1[1])/pow(p2[0]-p1[0],2)]) 21 | k = np.linalg.solve(A, b) 22 | return k 23 | 24 | # get_spline_points: get points along a spline defined by points and parameters kx, ky 25 | def get_spline_points(p_spline,kx,ky,z): 26 | 27 | x0 = p_spline[0][0] 28 | y0 = p_spline[0][1] 29 | z0 = p_spline[0][2] 30 | x1 = p_spline[1][0] 31 | y1 = p_spline[1][1] 32 | z1 = p_spline[1][2] 33 | x2 = p_spline[2][0] 34 | y2 = p_spline[2][1] 35 | z2 = p_spline[2][2] 36 | 37 | a01x = kx[0]*(z1-z0) - (x1-x0); 38 | a12x = kx[1]*(z2-z1) - (x2-x1); 39 | a01y = ky[0]*(z1-z0) - (y1-y0); 40 | a12y = ky[1]*(z2-z1) - (y2-y1); 41 | 42 | b01x = -kx[1]*(z1-z0) - (x1-x0); 43 | b12x = -kx[2]*(z2-z1) - (x2-x1); 44 | b01y = -ky[1]*(z1-z0) - (y1-y0); 45 | b12y = -ky[2]*(z2-z1) - (y2-y1); 46 | 47 | t01 = (z-z0)/(z1-z0) 48 | t12 = (z-z1)/(z2-z1) 49 | 50 | x01out = x0*(1-t01) + x1*t01 + np.multiply(np.multiply(t01,(1-t01)),(a01x*(1-t01) + b01x*t01)) 51 | x12out = x1*(1-t12) + x2*t12 + np.multiply(np.multiply(t12,(1-t12)),(a12x*(1-t12) + b12x*t12)) 52 | y01out = y0*(1-t01) + y1*t01 + np.multiply(np.multiply(t01,(1-t01)),(a01y*(1-t01) + b01y*t01)) 53 | y12out = y1*(1-t12) + y2*t12 + np.multiply(np.multiply(t12,(1-t12)),(a12y*(1-t12) + b12y*t12)) 54 | 55 | xout = np.multiply(x01out,(z < z1)) + np.multiply(x12out,(z >= z1)) 56 | yout = np.multiply(y01out,(z < z1)) + np.multiply(y12out,(z >= z1)) 57 | 58 | return (xout,yout) 59 | 60 | # gen_single_simtree: generate synthetic trees 61 | def gen_single_simtree(Nstem=300, Nfol=1000, model_params=None, tx=None, ty=None): 62 | 63 | # class IDs used for different point types 64 | class_id = {} 65 | class_id['folliage'] = 0 66 | class_id['stem'] = 1 67 | 68 | # Generate main stem parameters 69 | if tx is None: 70 | tx = 0 71 | if ty is None: 72 | ty = 0 73 | tz = 0 74 | hr = model_params['height_range'] 75 | h = hr[0] + (hr[1]-hr[0])*np.random.random() 76 | dr = model_params['diam_range'] 77 | r = 0.5*dr[0] + 0.5*(dr[1]-dr[0])*np.random.random() 78 | 79 | # randomise fork status 80 | rn = np.random.random() 81 | if rn < model_params['split_prob']: 82 | fork = 2 83 | else: 84 | fork = 1 85 | 86 | stem_data = [] 87 | 88 | # Initial spline for main stem 89 | p0 = [tx,ty,tz] 90 | sweeptopx = 2*model_params['tree_top_dist']*np.random.random()-model_params['tree_top_dist'] 91 | sweeptopy = 2*model_params['tree_top_dist']*np.random.random()-model_params['tree_top_dist'] 92 | tmdist = model_params['tree_mid_dist'] 93 | p1 = [tx+2*tmdist*np.random.random()-tmdist+0.2*sweeptopx,ty+2*tmdist*np.random.random()-tmdist+0.2*sweeptopy,0.5*h+(0.167*h*np.random.random())] 94 | p2 = [tx+sweeptopx,ty+sweeptopy,h] 95 | p_spline = np.array([p0, p1, p2]) 96 | kx = get_spline_params( (p_spline[0][2],p_spline[0][0]), (p_spline[1][2],p_spline[1][0]), (p_spline[2][2],p_spline[2][0])) 97 | ky = get_spline_params( (p_spline[0][2],p_spline[0][1]), (p_spline[1][2],p_spline[1][1]), (p_spline[2][2],p_spline[2][1])) 98 | 99 | if fork == 1: # Single stem 100 | 101 | zc = np.linspace(tz,h,num=20) 102 | (xc,yc) = get_spline_points(p_spline,kx,ky,zc) 103 | stem_data.append([xc,yc,zc,p_spline,kx,ky,[0,h]]) 104 | 105 | elif fork == 2: # Split/forked stem 106 | 107 | # Generate parameters for forks 108 | splhr = model_params['split_height_range'] 109 | hf = h*(splhr[0]+(splhr[1]-splhr[0])*np.random.random()) 110 | 111 | zc = np.linspace(tz,hf,num=20) 112 | (xc,yc) = get_spline_points(p_spline,kx,ky,zc) 113 | stem_data.append([xc,yc,zc,p_spline,kx,ky,[0,hf]]) 114 | 115 | dr = 2.0*np.random.random()+1.5 116 | theta = 2*pi*np.random.random() 117 | dx = dr*sin(theta) 118 | dy = dr*cos(theta) 119 | topxy1 = [sweeptopx+dx,sweeptopy+dy,h] 120 | l2 = (0.3*np.random.random()+0.7)*(h-hf)+hf 121 | 122 | topxy2 = [sweeptopx-dx,sweeptopy-dy,l2] 123 | 124 | bottomxy = [xc[-1],yc[-1],hf] 125 | dx1 = 0.3*(topxy1[0]-bottomxy[0]) 126 | dy1 = 0.3*(topxy1[1]-bottomxy[1]) 127 | dx2 = 0.3*(topxy2[0]-bottomxy[0]) 128 | dy2 = 0.3*(topxy2[1]-bottomxy[1]) 129 | bend = 0.3+0.1*np.random.random() 130 | midxy1 = [bottomxy[0]+dx1,bottomxy[1]+dy1,bend*(h-hf)+hf] 131 | midxy2 = [bottomxy[0]+dx2,bottomxy[1]+dy2,bend*(topxy2[2]-hf)+hf] 132 | 133 | p_spline = np.array([bottomxy, midxy1, topxy1]) 134 | kx = get_spline_params( (p_spline[0][2],p_spline[0][0]), (p_spline[1][2],p_spline[1][0]), (p_spline[2][2],p_spline[2][0])) 135 | ky = get_spline_params( (p_spline[0][2],p_spline[0][1]), (p_spline[1][2],p_spline[1][1]), (p_spline[2][2],p_spline[2][1])) 136 | zc = np.linspace(hf,topxy1[2],num=20) 137 | (xc,yc) = get_spline_points(p_spline,kx,ky,zc) 138 | stem_data.append([xc,yc,zc,p_spline,kx,ky,[hf,topxy1[2]]]) 139 | 140 | p_spline = np.array([bottomxy, midxy2, topxy2]) 141 | kx = get_spline_params( (p_spline[0][2],p_spline[0][0]), (p_spline[1][2],p_spline[1][0]), (p_spline[2][2],p_spline[2][0])) 142 | ky = get_spline_params( (p_spline[0][2],p_spline[0][1]), (p_spline[1][2],p_spline[1][1]), (p_spline[2][2],p_spline[2][1])) 143 | zc = np.linspace(hf,topxy2[2],num=20) 144 | (xc,yc) = get_spline_points(p_spline,kx,ky,zc) 145 | stem_data.append([xc,yc,zc,p_spline,kx,ky,[hf,topxy2[2]]]) 146 | 147 | # Generate stem mesh 148 | verts = [] 149 | faces = [] 150 | lv = 0 151 | for stem in stem_data: 152 | (xc,yc,zc,p,kx,ky,zrange) = stem 153 | Ndotsl = 12 154 | t = np.linspace(0,2*pi,Ndotsl+1) 155 | t = t[:-1] 156 | for i in range(len(zc)): 157 | rn = r*(1-(zc[i]/h)) 158 | xn = rn*np.sin(t)+xc[i] 159 | yn = rn*np.cos(t)+yc[i] 160 | for j in range(xn.shape[0]): 161 | verts.append([xn[j],yn[j],zc[i]]) 162 | for i in range(len(zc)-1): 163 | for j in range(t.shape[0]-1): 164 | faces.append([Ndotsl*i+j+1+lv,Ndotsl*i+j+lv,Ndotsl*(i+1)+j+lv,Ndotsl*(i+1)+j+1+lv]) 165 | faces.append([Ndotsl*i+lv,Ndotsl*i+t.shape[0]-1+lv,Ndotsl*(i+1)+t.shape[0]-1+lv,Ndotsl*(i+1)+lv]) 166 | lv = len(verts) 167 | 168 | mesh = trimesh.Trimesh(vertices=verts,faces=faces) 169 | 170 | # Sample points from stem meshes 171 | points = mesh.sample(Nstem) 172 | points_all = np.concatenate((points,class_id['stem']*np.ones((Nstem,1))),axis=1) 173 | 174 | # Generate branches and foliage 175 | h1r = [model_params['min_can_height'][0], model_params['min_can_height'][1]] 176 | h2r = [model_params['max_can_width_height'][0], model_params['max_can_width_height'][1]] 177 | h1 = h*(h1r[0]+(h1r[1]-h1r[0])*np.random.random()) 178 | h2 = h*(h2r[0]+(h2r[1]-h2r[0])*np.random.random()) 179 | 180 | branchz1 = h1+(h2-h1)*np.random.random(np.random.randint(0.166*model_params['num_branches'][0],0.3*model_params['num_branches'][1])) 181 | branchz2 = h2+(h-h2)*np.random.random(np.random.randint(fork*0.833*model_params['num_branches'][0],fork*0.7*model_params['num_branches'][1])) 182 | 183 | branchz = np.concatenate((branchz1,branchz2)) 184 | xyc = [] 185 | for stem in stem_data: 186 | (x,y,z,p_spline,kx,ky,zrange) = stem 187 | (xc,yc) = get_spline_points(p_spline,kx,ky,branchz) 188 | xyc.append((xc,yc)) 189 | 190 | # Generate mesh data for branches and foliage 191 | verts = [] 192 | faces = [] 193 | c = 0 194 | rolloff = 1.0 195 | rolloffh = h2 196 | rolloffh2 = h1 197 | for i in range(len(branchz)): 198 | 199 | # determine which stem to put on 200 | okstems = [] 201 | for (j,stem) in enumerate(stem_data): 202 | (x,y,z,p_spline,kx,ky,zrange) = stem 203 | if branchz[i] >= zrange[0] and branchz[i] <= zrange[1]: 204 | okstems.append(j) 205 | if len(okstems) == 0: 206 | pass 207 | elif len(okstems) == 1: 208 | j = okstems[0] 209 | xc = xyc[j][0] 210 | yc = xyc[j][1] 211 | else: 212 | j = okstems[np.random.choice(len(okstems),1)[0]] 213 | xc = xyc[j][0] 214 | yc = xyc[j][1] 215 | 216 | if (i % 10) == 0: 217 | gap = np.random.random() 218 | gap2 = (2*pi-gap)*np.random.random() 219 | theta = 2*pi*np.random.random() 220 | if theta > gap2 and theta < (gap2+gap): 221 | continue 222 | rn = r*(1-(branchz[i]/h)) 223 | rout = (model_params['max_can_width']/7.0)*sqrt(h-branchz[i]) 224 | if branchz[i] < rolloffh2: 225 | rout = 0 226 | elif branchz[i] < rolloffh: 227 | rout = rout-(rolloff*rout*((rolloffh-branchz[i])/rolloffh)) 228 | 229 | rout2 = rout+0.4*rout*np.random.random()-0.2*rout 230 | thick = 0.2*rout2 231 | verts.append([rn*sin(theta)+xc[i],rn*cos(theta)+yc[i],branchz[i]]) 232 | verts.append([(rout2/3)*sin(theta)+thick*cos(theta)+xc[i],(rout2/3)*cos(theta)-thick*sin(theta)+yc[i],branchz[i]+np.random.random()]) 233 | verts.append([(rout2/3)*sin(theta)-thick*cos(theta)+xc[i],(rout2/3)*cos(theta)+thick*sin(theta)+yc[i],branchz[i]+np.random.random()]) 234 | verts.append([(2*rout2/3)*sin(theta)+thick*cos(theta)+xc[i],(2*rout2/3)*cos(theta)-thick*sin(theta)+yc[i],branchz[i]+np.random.random()]) 235 | verts.append([(2*rout2/3)*sin(theta)-thick*cos(theta)+xc[i],(2*rout2/3)*cos(theta)+thick*sin(theta)+yc[i],branchz[i]+np.random.random()]) 236 | verts.append([rout2*sin(theta)+xc[i],rout2*cos(theta)+yc[i],branchz[i]+np.random.random()]) 237 | 238 | faces.append([6*c,6*c+1,6*c+2]) 239 | faces.append([6*c+1,6*c+2,6*c+4]) 240 | faces.append([6*c+1,6*c+4,6*c+3]) 241 | faces.append([6*c+3,6*c+4,6*c+5]) 242 | c += 1 243 | 244 | mesh2 = trimesh.Trimesh(vertices=verts,faces=faces) 245 | 246 | # Sample foliage points 247 | points = mesh2.sample(Nfol) 248 | points[:,2] += model_params['foliage_noise']*np.random.random(points.shape[0]) 249 | points_class = np.concatenate((points,class_id['folliage']*np.ones((Nfol,1))),axis=1) 250 | 251 | points_all = np.concatenate((points_all,points_class)) 252 | 253 | return points_all 254 | 255 | # gen_simtree: Generates sample point cloud of synthetically-generated tree 256 | def gen_simtree(Np=1024, stem_frac_lambda=0.3, model_params=None): 257 | 258 | # create example default parameters if none specified 259 | if model_params is None: 260 | model_params = {} 261 | model_params['height_range'] = [30, 50] 262 | model_params['diam_range'] = [0.5, 1] 263 | model_params['split_height_range'] = [0.15, 0.5] 264 | model_params['split_prob'] = 0.5 265 | model_params['num_branches'] = [60, 100] 266 | model_params['min_can_height'] = [0.2, 0.5] 267 | model_params['max_can_width'] = 7.0 268 | model_params['max_can_width_height'] = [0.4, 0.8] 269 | model_params['tree_top_dist'] = 2.5 270 | model_params['tree_mid_dist'] = 0.5 271 | model_params['foliage_noise'] = 0.5 272 | 273 | # stem_frac_lambda: fraction of stem hits relative to foliage 274 | if isinstance(stem_frac_lambda,list): # stem_frac within a specified range 275 | stem_frac_lambda = stem_frac_lambda[0] + np.random.random()*(stem_frac_lambda[1]-stem_frac_lambda[0]) 276 | 277 | # Work out numbers of points 278 | Nground = 0 279 | Nstem = int(stem_frac_lambda*Np) 280 | Nfol = Np-(Nground+Nstem) 281 | 282 | # Generate initial point cloud 283 | points_all = np.zeros((0,4)) 284 | 285 | # Generate stem and foliage 286 | points_tree = gen_single_simtree(Nstem=Nstem,Nfol=Nfol,model_params=model_params) 287 | 288 | points_all = np.concatenate((points_all,points_tree)) 289 | 290 | return points_all 291 | 292 | 293 | --------------------------------------------------------------------------------