SimpleCrownGenerator

  1# coding: utf-8
  2# Author: Jianbo Qi
  3# Date: 2020.4.16
  4# Generate tree crowns with randomly distributed leaves
  5import numpy as np
  6from scipy.linalg import expm, norm
  7import math, sys
  8import trimesh
  9import random
 10from scipy.spatial import cKDTree
 11import alphashape
 12from AlphaShapeCGAL import AlphaShapeCGAL
 13import tempfile
 14import shutil
 15
 16
 17def agd_amp_max(sig_l, sig_r, gamma):
 18    tmp = math.sqrt(math.gamma(1 / gamma) / math.gamma(3 / gamma))
 19    beta_l = sig_l * tmp
 20    beta_r = sig_r * tmp
 21    A = gamma / ((beta_l + beta_r) * math.gamma(1 / gamma))
 22    return A
 23
 24
 25def agd(x, sig_l, sig_r, gamma=2.0, m=0.0):
 26    """
 27    Asymmetric gaussian distribution
 28    """
 29    tmp = math.sqrt(math.gamma(1 / gamma) / math.gamma(3 / gamma))
 30    beta_l = sig_l * tmp
 31    beta_r = sig_r * tmp
 32    A = gamma / ((beta_l + beta_r) * math.gamma(1 / gamma))
 33    if x <= m:
 34        y = A * math.exp(-(-(x - m) / beta_l) ** gamma)
 35    else:
 36        y = A * math.exp(-((x - m) / beta_r) ** gamma)
 37    return y
 38
 39
 40class LAD:
 41    UNIFORM = "Uniform"
 42    SPHERICAL = "Spherical"
 43    ERECTOPHILE = "Erectophile"
 44    EXTREMOPHILE = "Extremophile"
 45    PLANOPHILE = "Planophile"
 46    PLAGIOPHILE = "Plagiophile"
 47
 48
 49class CrownShape:
 50    CUBE = "Cube"
 51    ELLIPSOID = "Ellipsoid"
 52    CYLINDER = "Cylinder"
 53    CONE = "Cone"
 54    ASYMMETRIC_GAUSSIAN = "Asymmeric_Gaussian"
 55    # USER_DEFINED_CURVE = "USER_DEFINED_CURVE"
 56
 57
 58class LeafShape:
 59    SQUARE = "Square"
 60    DISK = "Disk"
 61
 62
 63class CrownGenerator:
 64    def __init__(self):
 65        self.leaf_angle_dist = LAD.SPHERICAL
 66        self.leaf_volume_density = 1.0
 67        self.crown_diameter_SN = 1
 68        self.crown_diameter_EW = 1
 69        self.crown_height = 3
 70        self.crown_shape = CrownShape.ELLIPSOID
 71        self.leaf_shape = LeafShape.SQUARE
 72        self.leaf_num_triangles = 12
 73        self.single_leaf_area = 0.01
 74
 75        # generate crown boundary only
 76        self.generate_crown_only = False
 77        self.clumped_volume = 0  # If the leaf is turbid, and clumping is less than 1, store the clumped volume
 78        self.total_wood_area = 0
 79
 80        # trunk
 81        self.has_trunk = False
 82        self.trunk_height = 3
 83        self.dbh = 0.2
 84        # This value divides trunk into two segments, for applications such as forest fire severity
 85        self.trunk_division_height = 0
 86
 87        # If generate branches, when trunk is generated
 88        self.has_branches = False
 89        self.branch_vertical_angle = 45  # degree, angle between [0, 1, 0]
 90        self.vertical_distance_between_branch = 0.8  # vertical distance between branches, starting from trunk height
 91        self.num_branches_each_height = 3  # The number of branches at each branch height
 92        self.branch_radius_scale = 0.33  # the scale factor for branch base radius, compared with parent trunk
 93        self.random_angle_range = 20  # the random angle range to offset vertical and horizontal angles
 94
 95        # clumping
 96        # [0, 1] 1 for random, 0 for total clumping
 97        self.clumping_factor = 1.0
 98        # [0, 1) clustering leaf to branch top factor: 1 around branch top, 0 around whole branch
 99        self.leaf_distance_factor_to_branch_root = 0.3
100
101        # trunk for asymmetric gaussian
102        self.sig_upper = 2
103        self.sig_lower = 4
104        self.gamma_shape = 6
105
106        # curve for user defined, [(Height, Crown_Radius)]
107        # self.user_curve = [(0, 0.4), (0.2, 0.5)]
108
109        self._num_of_leaves = 100
110        self._num_sigma = 2.0
111
112    def get_crown_volume(self):
113        volume = 0.0
114        if self.crown_shape == CrownShape.CUBE:
115            volume = self.crown_diameter_SN * self.crown_diameter_EW * self.crown_height
116        elif self.crown_shape == CrownShape.ELLIPSOID:
117            volume = 4 * math.pi * (0.5 * self.crown_diameter_EW) * (0.5 * self.crown_diameter_SN) * (
118                        0.5 * self.crown_height) / 3.0
119        elif self.crown_shape == CrownShape.CYLINDER:
120            volume = math.pi * (0.5 * self.crown_diameter_EW) * (0.5 * self.crown_diameter_SN) * self.crown_height
121        elif self.crown_shape == CrownShape.CONE:
122            volume = math.pi * (0.5 * self.crown_diameter_EW) * (0.5 * self.crown_diameter_SN) * self.crown_height / 3.0
123        elif self.crown_shape == CrownShape.ASYMMETRIC_GAUSSIAN:
124            num_sigma = self._num_sigma
125            n_bins = 499
126            x = np.linspace(-num_sigma * self.sig_lower, num_sigma * self.sig_upper, n_bins + 1)
127            step = self.crown_height / n_bins
128            amp_max = agd_amp_max(self.sig_lower, self.sig_upper, self.gamma_shape)
129            y = []
130            for xx in x:
131                yy = agd(xx, self.sig_lower, self.sig_upper, gamma=self.gamma_shape)
132                y.append(self.crown_diameter_SN * 0.5 * yy / amp_max)
133            for i in range(len(x) - 1):
134                s = (1.0 / 3.0) * math.pi * step * (y[i] ** 2 + y[i + 1] ** 2 + y[i] * y[i + 1])
135                volume += s
136        return volume
137
138    def __tree_var_to_asymmetric_gaussian_xy(self, h, dist_trunk):
139        """
140        This function compute the x value, given the height value in the crown
141        """
142        num_sigma = self._num_sigma
143        min_x, max_x = -num_sigma * self.sig_lower, num_sigma * self.sig_upper
144        x = min_x + (max_x - min_x) * (h / self.crown_height)
145        amp_max = agd_amp_max(self.sig_lower, self.sig_upper, self.gamma_shape)
146        y = dist_trunk / (0.5 * self.crown_diameter_SN) * amp_max
147        return x, y, amp_max
148
149    # considering trunk, if leaf position is within radius, it should be regenerated.
150    def consider_by_trunk_radius(self, x, y, z, leaf_length):
151        if not self.has_trunk:
152            return [x, y, z]  # if no trunk, directly return the original xyz
153        else:
154            h = self.trunk_height + y
155            trunk_radius = self.get_trunk_radius_at_height(h)
156            dist_to_center = np.sqrt(x * x + z * z)
157            while dist_to_center < trunk_radius + leaf_length:
158                if self.crown_shape == CrownShape.CUBE:
159                    x = -0.5 * self.crown_diameter_EW + np.random.rand() * self.crown_diameter_EW
160                    z = -0.5 * self.crown_diameter_SN + np.random.rand() * self.crown_diameter_SN
161                    y = np.random.rand() * self.crown_height
162                elif self.crown_shape == CrownShape.ELLIPSOID:
163                    phi = np.random.rand() * np.pi * 2  # randomly azimuth angle
164                    theta = np.arccos(2 * np.random.rand() - 1)  # randomly zenith angle
165                    r = np.cbrt(np.random.rand())
166                    x = r * self.crown_diameter_EW * 0.5 * np.sin(theta) * np.cos(phi)
167                    z = r * self.crown_diameter_SN * 0.5 * np.sin(theta) * np.sin(phi)
168                    y = r * self.crown_height * 0.5 * np.cos(theta) + self.crown_height * 0.5
169                elif self.crown_shape == CrownShape.CYLINDER:
170                    phi = np.random.rand() * np.pi * 2  # randomly azimuth angle
171                    r = np.sqrt(np.random.rand())
172                    x = r * self.crown_diameter_EW * 0.5 * np.cos(phi)
173                    z = r * self.crown_diameter_SN * 0.5 * np.sin(phi)
174                    y = np.random.rand() * self.crown_height
175                elif self.crown_shape == CrownShape.CONE:
176                    phi = np.random.rand() * np.pi * 2  # randomly azimuth angle
177                    h = self.crown_height * (np.random.rand()) ** (1 / 3)
178                    rnd = np.random.rand()
179                    rew = (0.5 * self.crown_diameter_EW / self.crown_height) * h * np.sqrt(rnd)
180                    rsn = (0.5 * self.crown_diameter_SN / self.crown_height) * h * np.sqrt(rnd)
181                    x = rew * np.cos(phi)
182                    z = rsn * np.sin(phi)
183                    y = self.crown_height - h
184                elif self.crown_shape == CrownShape.ASYMMETRIC_GAUSSIAN:
185                    while True:
186                        y = self.crown_height * np.random.rand()
187                        x = -0.5 * self.crown_diameter_EW + np.random.rand() * self.crown_diameter_EW
188                        z = -0.5 * self.crown_diameter_SN + np.random.rand() * self.crown_diameter_SN
189                        dist_to_center1 = np.sqrt(x * x + z * z)
190                        asym_x, asym_y, amp_max = self.__tree_var_to_asymmetric_gaussian_xy(y, dist_to_center1)
191                        pred_y = agd(asym_x, self.sig_lower, self.sig_upper, self.gamma_shape)
192                        if asym_y <= pred_y:
193                            break
194                dist_to_center = np.sqrt(x * x + z * z)
195            return [x, y, z]
196
197    def _generate_leaf_pos(self):
198        leaf_length = self._calculate_leaf_length()
199        all_pos = []
200        if self.crown_shape == CrownShape.CUBE:
201            for i in range(self._num_of_leaves):
202                x = -0.5 * self.crown_diameter_EW + np.random.rand() * self.crown_diameter_EW
203                z = -0.5 * self.crown_diameter_SN + np.random.rand() * self.crown_diameter_SN
204                y = np.random.rand() * self.crown_height
205                all_pos.append(self.consider_by_trunk_radius(x, y, z, leaf_length))
206
207        elif self.crown_shape == CrownShape.ELLIPSOID:
208            for i in range(self._num_of_leaves):
209                phi = np.random.rand() * np.pi * 2  # randomly azimuth angle
210                theta = np.arccos(2 * np.random.rand() - 1)  # randomly zenith angle
211                r = np.cbrt(np.random.rand())
212                x = r * self.crown_diameter_EW * 0.5 * np.sin(theta) * np.cos(phi)
213                z = r * self.crown_diameter_SN * 0.5 * np.sin(theta) * np.sin(phi)
214                y = r * self.crown_height * 0.5 * np.cos(theta) + self.crown_height * 0.5
215                all_pos.append(self.consider_by_trunk_radius(x, y, z, leaf_length))
216        elif self.crown_shape == CrownShape.CYLINDER:
217            for i in range(self._num_of_leaves):
218                phi = np.random.rand() * np.pi * 2  # randomly azimuth angle
219                r = np.sqrt(np.random.rand())
220                x = r * self.crown_diameter_EW * 0.5 * np.cos(phi)
221                z = r * self.crown_diameter_SN * 0.5 * np.sin(phi)
222                y = np.random.rand() * self.crown_height
223                all_pos.append(self.consider_by_trunk_radius(x, y, z, leaf_length))
224        elif self.crown_shape == CrownShape.CONE:
225            for i in range(self._num_of_leaves):
226                phi = np.random.rand() * np.pi * 2  # randomly azimuth angle
227                h = self.crown_height * (np.random.rand()) ** (1 / 3)
228                rnd = np.random.rand()
229                rew = (0.5 * self.crown_diameter_EW / self.crown_height) * h * np.sqrt(rnd)
230                rsn = (0.5 * self.crown_diameter_SN / self.crown_height) * h * np.sqrt(rnd)
231                x = rew * np.cos(phi)
232                z = rsn * np.sin(phi)
233                y = self.crown_height - h
234                all_pos.append(self.consider_by_trunk_radius(x, y, z, leaf_length))
235        elif self.crown_shape == CrownShape.ASYMMETRIC_GAUSSIAN:
236            while True:
237                y = self.crown_height * np.random.rand()
238                x = -0.5 * self.crown_diameter_EW + np.random.rand() * self.crown_diameter_EW
239                z = -0.5 * self.crown_diameter_SN + np.random.rand() * self.crown_diameter_SN
240                dist_to_center = np.sqrt(x * x + z * z)
241                asym_x, asym_y, amp_max = self.__tree_var_to_asymmetric_gaussian_xy(y, dist_to_center)
242                pred_y = agd(asym_x, self.sig_lower, self.sig_upper, self.gamma_shape)
243                if asym_y <= pred_y:
244                    all_pos.append(self.consider_by_trunk_radius(x, y, z, leaf_length))
245                if len(all_pos) == self._num_of_leaves:
246                    break
247        # elif self.crown_shape == CrownShape.USER_DEFINED_CURVE:
248        #     while True:
249        #         y = self.crown_height * np.random.rand()
250        #         x = -0.5 * self.crown_diameter_EW + np.random.rand() * self.crown_diameter_EW
251        #         z = -0.5 * self.crown_diameter_SN + np.random.rand() * self.crown_diameter_SN
252        #         dist_to_center = np.sqrt(x * x + z * z)
253        return all_pos
254
255    def _generate_leaf_normal(self):
256        '''
257        Sphrical: 球形状
258        Uniform:统一型
259        Planophile:平面型
260        Erectophile:竖直型
261        Plagiophile:倾斜型
262        Extremophile:极端型
263        '''
264        all_normals = []
265        if self.leaf_angle_dist == LAD.SPHERICAL:
266            for i in range(self._num_of_leaves):
267                phi = np.random.rand() * np.pi * 2  # randomly azimuth angle
268                theta = np.arccos(np.random.rand())  # randomly zenith angle
269                rx = -np.sin(theta) * np.cos(phi)
270                rz = np.sin(theta) * np.sin(phi)
271                ry = np.cos(theta)
272                all_normals.append([rx, ry, rz])
273            # f = open(r"D:\LESS\simulations\LESSMedium\SimpleHorizontalLayerRAMI\HOM03_ERE_scene.def")
274            # for line in f:
275            #     arr = list(map(lambda x: float(x), line.split()))
276            #     all_normals.append([-arr[4], arr[6], arr[5]])
277        if self.leaf_angle_dist == LAD.UNIFORM:
278            for i in range(self._num_of_leaves):
279                phi = np.random.rand() * np.pi * 2  # randomly azimuth angle
280                theta = np.random.rand() * np.pi * 0.5  # randomly zenith angle
281                rx = -np.sin(theta) * np.cos(phi)
282                rz = np.sin(theta) * np.sin(phi)
283                ry = np.cos(theta)
284                all_normals.append([rx, ry, rz])
285        if self.leaf_angle_dist == LAD.PLANOPHILE:
286            while True:
287                theta = np.random.rand() * np.pi * 0.5
288                y = (2 + 2 * np.cos(2 * theta)) / np.pi
289                rnd_y = np.random.rand() * 4 / np.pi
290                if rnd_y <= y:  # accept
291                    phi = np.random.rand() * np.pi * 2  # randomly azimuth angle
292                    rx = -np.sin(theta) * np.cos(phi)
293                    rz = np.sin(theta) * np.sin(phi)
294                    ry = np.cos(theta)
295                    all_normals.append([rx, ry, rz])
296                    if len(all_normals) == self._num_of_leaves:
297                        break
298        if self.leaf_angle_dist == LAD.ERECTOPHILE:  # 竖直型
299            while True:
300                theta = np.random.rand() * np.pi * 0.5
301                y = (2 - 2 * np.cos(2 * theta)) / np.pi
302                rnd_y = np.random.rand() * 4 / np.pi
303                if rnd_y <= y:  # accept
304                    phi = np.random.rand() * np.pi * 2  # randomly azimuth angle
305                    rx = -np.sin(theta) * np.cos(phi)
306                    rz = np.sin(theta) * np.sin(phi)
307                    ry = np.cos(theta)
308                    all_normals.append([rx, ry, rz])
309                    if len(all_normals) == self._num_of_leaves:
310                        break
311        if self.leaf_angle_dist == LAD.PLAGIOPHILE:  # 倾斜型
312            while True:
313                theta = np.random.rand() * np.pi * 0.5
314                y = (2 - 2 * np.cos(4 * theta)) / np.pi
315                rnd_y = np.random.rand() * 4 / np.pi
316                if rnd_y <= y:  # accept
317                    phi = np.random.rand() * np.pi * 2  # randomly azimuth angle
318                    rx = -np.sin(theta) * np.cos(phi)
319                    rz = np.sin(theta) * np.sin(phi)
320                    ry = np.cos(theta)
321                    all_normals.append([rx, ry, rz])
322                    if len(all_normals) == self._num_of_leaves:
323                        break
324        if self.leaf_angle_dist == LAD.EXTREMOPHILE:  # 极端
325            while True:
326                theta = np.random.rand() * np.pi * 0.5
327                y = (2 + 2 * np.cos(4 * theta)) / np.pi
328                rnd_y = np.random.rand() * 4 / np.pi
329                if rnd_y <= y:  # accept
330                    phi = np.random.rand() * np.pi * 2  # randomly azimuth angle
331                    rx = -np.sin(theta) * np.cos(phi)
332                    rz = np.sin(theta) * np.sin(phi)
333                    ry = np.cos(theta)
334                    all_normals.append([rx, ry, rz])
335                    if len(all_normals) == self._num_of_leaves:
336                        break
337        return all_normals
338
339    @staticmethod
340    def _M(axis, theta):
341        return expm(np.cross(np.eye(3), axis / norm(axis) * theta))
342
343    def _generate_single_leaf(self, leaf_pos, leaf_normal, leaf_length):
344        leaf_side_length = leaf_length
345        # generate leaf according to normal
346        if self.leaf_shape == LeafShape.SQUARE:
347            # First, find a arbitrary vector which is not parallel with normal
348            # and do cross product, the resulting vector is in leaf plane
349            arv = np.array([0, 0, 1])
350            v = np.cross(leaf_normal, arv)
351            v = v / np.linalg.norm(v)
352            p1 = leaf_side_length * np.sqrt(2) * 0.5 * v + leaf_pos
353            v = np.cross(leaf_normal, v)
354            p2 = leaf_side_length * np.sqrt(2) * 0.5 * v + leaf_pos
355            v = np.cross(leaf_normal, v)
356            p3 = leaf_side_length * np.sqrt(2) * 0.5 * v + leaf_pos
357            v = np.cross(leaf_normal, v)
358            p4 = leaf_side_length * np.sqrt(2) * 0.5 * v + leaf_pos
359            return [p1, p2, p3, p4]
360        elif self.leaf_shape == LeafShape.DISK:  # leaf_side_length is leaf radius
361            # First, find a arbitrary vector which is not parallel with normal
362            # and do cross product, the resulting vector is in leaf plane
363            pts = []
364            rot_rad = np.pi * 2 / self.leaf_num_triangles
365            arv = np.array([0, 0, 1])
366            v0 = np.cross(leaf_normal, arv)
367            v0 = v0 / np.linalg.norm(v0)
368            p1 = leaf_side_length * v0 + leaf_pos  # 第一个点
369            pts.append(p1)
370            for i in range(0, self.leaf_num_triangles - 1):
371                m0 = CrownGenerator._M(np.array(leaf_normal), rot_rad * (i + 1))
372                newv = np.dot(m0, v0)
373                newv = newv / np.linalg.norm(newv)
374                newp = leaf_side_length * newv + leaf_pos  # 第一个点
375                pts.append(newp)
376            return pts
377        else:
378            print("Leaf shape is not supported.")
379
380    def _calculate_leaf_length(self):
381        if self.leaf_shape == LeafShape.SQUARE:
382            return np.sqrt(self.single_leaf_area)
383        if self.leaf_shape == LeafShape.DISK:
384            theta = np.pi * 2 / self.leaf_num_triangles
385            tri_leaf = self.single_leaf_area / self.leaf_num_triangles
386            leaf_radius = np.sqrt(2 * tri_leaf / np.sin(theta))
387            return leaf_radius
388
389    def __compute_leaf_numbers(self):
390        crown_volume = self.get_crown_volume()
391        tot_area = crown_volume*self.leaf_volume_density
392        self._num_of_leaves = int(tot_area / self.single_leaf_area)
393        print("Crown volume:", "%.4f" % crown_volume)
394        print("Number of leaves:", self._num_of_leaves)
395        sys.stdout.flush()
396
397    def __generate_crown_boundary(self, h_of_first_branch):
398        mesh = None
399        bound_box = np.array([self.crown_diameter_EW, self.crown_height, self.crown_diameter_SN])
400        center_pt = np.array([0, h_of_first_branch+0.5*self.crown_height, 0])
401        if self.crown_shape == CrownShape.CUBE:
402            mesh = trimesh.creation.box(extents=bound_box)
403            mesh.apply_translation(center_pt)
404        elif self.crown_shape == CrownShape.ELLIPSOID:
405            mesh = trimesh.creation.icosphere()
406            mesh.apply_scale(0.5 * bound_box)
407            mesh.apply_translation(center_pt)
408        elif self.crown_shape == CrownShape.CYLINDER:
409            mesh = trimesh.creation.cylinder(1, 1)
410            mesh.vertices[:, [1, 2]] = mesh.vertices[:, [2, 1]]
411            mesh.faces[:, [1, 2]] = mesh.faces[:, [2, 1]]
412            mesh.apply_scale([0.5 * bound_box[0], bound_box[1], 0.5 * bound_box[2]])
413            mesh.apply_translation(center_pt)
414        elif self.crown_shape == CrownShape.CONE:
415            mesh = trimesh.creation.cone(1, 1)
416            mesh.vertices[:, [1, 2]] = mesh.vertices[:, [2, 1]]
417            mesh.faces[:, [1, 2]] = mesh.faces[:, [2, 1]]
418            mesh.apply_scale([0.5 * bound_box[0], bound_box[1], 0.5 * bound_box[2]])
419            center_pt = np.array([0, h_of_first_branch, 0])
420            mesh.apply_translation(center_pt)
421        elif self.crown_shape == CrownShape.ASYMMETRIC_GAUSSIAN:
422            num_sigma = self._num_sigma
423            n_bins = 199
424            min_bound = -num_sigma * self.sig_lower
425            max_bound = num_sigma * self.sig_upper
426            x_locals = np.linspace(min_bound, max_bound, n_bins + 1)
427            step = self.crown_height / n_bins
428            amp_max = agd_amp_max(self.sig_lower, self.sig_upper, self.gamma_shape)
429            xy = []
430            for x_local in x_locals:
431                y_local = agd(x_local, self.sig_lower, self.sig_upper, gamma=self.gamma_shape)
432                y_global = self.crown_diameter_SN * 0.5 * y_local / amp_max
433                x_global = h_of_first_branch + self.crown_height*(x_local - min_bound) / (max_bound - min_bound)
434                xy.append([y_global, x_global])
435            xy[0][0]=0
436            xy[-1][0]=0
437            mesh = trimesh.creation.revolve(np.array(xy))
438            mesh.vertices[:, [1, 2]] = mesh.vertices[:, [2, 1]]
439            mesh.faces[:, [1, 2]] = mesh.faces[:, [2, 1]]
440        return mesh
441
442    def get_trunk_radius_at_height(self, h):
443        dbh_radis = 0.5 * self.dbh
444        ground_radius = dbh_radis * (self.trunk_height + self.crown_height) / (
445                    self.trunk_height + self.crown_height - 1.3)
446        if self.trunk_height + self.crown_height <= 1.3:
447            ground_radius = dbh_radis
448        division_radius = ground_radius * (self.trunk_height + self.crown_height - h) / (
449                    self.trunk_height + self.crown_height)
450        if division_radius < 0:
451            division_radius = 0
452        return division_radius
453
454    def get_trunk_surface_area(self):
455        dbh_radis = 0.5 * self.dbh
456        ground_radius = dbh_radis * (self.trunk_height + self.crown_height) / (
457                self.trunk_height + self.crown_height - 1.3)
458        if self.trunk_height + self.crown_height <= 1.3:
459            ground_radius = dbh_radis
460        h = self.trunk_height + self.crown_height
461        return math.pi*ground_radius*math.sqrt(h*h+ground_radius*ground_radius)
462
463    def get_trunk_volume(self):
464        dbh_radis = 0.5 * self.dbh
465        ground_radius = dbh_radis * (self.trunk_height + self.crown_height) / (
466                self.trunk_height + self.crown_height - 1.3)
467        if self.trunk_height + self.crown_height <= 1.3:
468            ground_radius = dbh_radis
469        return math.pi*ground_radius*ground_radius*(self.trunk_height + self.crown_height)/3
470
471
472    def generate_crown(self, dist_obj_path):
473        f_out = open(dist_obj_path, "w")
474        trunk_start_index = 0
475        h_of_first_branch = 0
476        if self.has_trunk:
477            h_of_first_branch = self.trunk_height
478        crown_mesh = None
479        # trunk
480        branch_points = []
481        branch_ids = []
482        if self.has_trunk:
483            points_lower = []
484            points_upper = []
485            points_ground = []
486            points_division = []
487            f_out.write("g trunk\n")
488            dbh_radis = 0.5 * self.dbh
489            ground_radius = dbh_radis*(self.trunk_height + self.crown_height)/(self.trunk_height + self.crown_height-1.3)
490            if self.trunk_height + self.crown_height <= 1.3:
491                print("Total tree height is less than 1.3, using DBH as tree base radius.")
492                ground_radius = dbh_radis
493            division_radius = ground_radius*(self.trunk_height + self.crown_height-self.trunk_division_height)/(self.trunk_height + self.crown_height)
494            if self.trunk_height + self.crown_height-self.trunk_division_height <=0:
495                self.trunk_division_height = self.trunk_height + self.crown_height
496                division_radius = 0.001
497
498            for i in range(0, 360, 15):
499                angle_rad = np.radians(i)
500                x_ground = -ground_radius * np.cos(angle_rad)
501                z_ground = ground_radius * np.sin(angle_rad)
502                x_division = -division_radius * np.cos(angle_rad)
503                z_division = division_radius * np.sin(angle_rad)
504                x_top = -0.001 * np.cos(angle_rad)
505                z_top = 0.001 * np.sin(angle_rad)
506                points_lower.append([x_division, self.trunk_division_height, z_division])
507                points_upper.append([x_top, self.trunk_height + self.crown_height, z_top])
508                if self.trunk_division_height > 0:
509                    points_ground.append([x_ground, 0, z_ground])
510                    points_division.append([x_division, self.trunk_division_height, z_division])
511            points_lower.append(points_lower[0])
512            points_upper.append(points_upper[0])
513            if self.trunk_division_height > 0:
514                points_ground.append(points_ground[0])
515                points_division.append(points_division[0])
516            for i in range(0, 24):  # 12个面元
517                f_out.write("v %.4f %.4f %.4f\n" % (points_lower[i][0], points_lower[i][1], points_lower[i][2]))
518                f_out.write(
519                    "v %.4f %.4f %.4f\n" % (points_lower[i + 1][0], points_lower[i + 1][1], points_lower[i + 1][2]))
520                f_out.write(
521                    "v %.4f %.4f %.4f\n" % (points_upper[i + 1][0], points_upper[i + 1][1], points_upper[i + 1][2]))
522                f_out.write("v %.4f %.4f %.4f\n" % (points_upper[i][0], points_upper[i][1], points_upper[i][2]))
523                f_out.write(
524                    "f %d %d %d %d\n" % (trunk_start_index + 1,
525                                         trunk_start_index + 2,
526                                         trunk_start_index + 3,
527                                         trunk_start_index + 4))
528                trunk_start_index += 4
529            #  trunk top
530            for i in range(0, 24):  # 12 top triangles
531                f_out.write(
532                    "v %.4f %.4f %.4f\n" % (points_upper[i][0], points_upper[i][1], points_upper[i][2]))
533                f_out.write(
534                    "v %.4f %.4f %.4f\n" % (points_upper[i + 1][0], points_upper[i + 1][1], points_upper[i + 1][2]))
535                f_out.write(
536                    "v %.4f %.4f %.4f\n" % (0, self.trunk_height, 0))
537                f_out.write(
538                    "f %d %d %d\n" % (trunk_start_index + 1,
539                                      trunk_start_index + 2,
540                                      trunk_start_index + 3))
541                trunk_start_index += 3
542
543            if self.trunk_division_height > 0:
544                f_out.write("\ng trunk_bottom\n")
545                for i in range(0, 24):  # 12个面元
546                    f_out.write("v %.4f %.4f %.4f\n" % (points_ground[i][0], points_ground[i][1], points_ground[i][2]))
547                    f_out.write(
548                        "v %.4f %.4f %.4f\n" % (points_ground[i + 1][0], points_ground[i + 1][1], points_ground[i + 1][2]))
549                    f_out.write(
550                        "v %.4f %.4f %.4f\n" % (points_division[i + 1][0], points_division[i + 1][1], points_division[i + 1][2]))
551                    f_out.write("v %.4f %.4f %.4f\n" % (points_division[i][0], points_division[i][1], points_division[i][2]))
552                    f_out.write(
553                        "f %d %d %d %d\n" % (trunk_start_index + 1,
554                                             trunk_start_index + 2,
555                                             trunk_start_index + 3,
556                                             trunk_start_index + 4))
557                    trunk_start_index += 4
558
559            branch_surface_area = 0
560            branch_volume = 0
561            if self.has_branches:
562                f_out.write("\ng branch\n")
563                if crown_mesh is None:
564                    crown_mesh = self.__generate_crown_boundary(h_of_first_branch)
565                branch_vertical_angle_center = self.branch_vertical_angle/180*math.pi
566                branch_horizontal_angle_interval = 2*math.pi/self.num_branches_each_height
567                num_branch_heights = int(self.crown_height/self.vertical_distance_between_branch)
568                index = 0
569                branch_index = 0
570                for i in range(num_branch_heights):
571                    branch_height = h_of_first_branch + i*self.vertical_distance_between_branch + 0.0001
572                    h_radius = self.get_trunk_radius_at_height(branch_height)
573                    angle_start = 0
574                    if index % 2 == 1:
575                        angle_start = 0.5 * branch_horizontal_angle_interval
576                    for j in range(self.num_branches_each_height):  # for each branch
577                        random_angle_range = self.random_angle_range/180*math.pi
578                        branch_vertical_angle = branch_vertical_angle_center + random_angle_range*(random.random()*2-1)
579                        horizontal_angle = angle_start + j*branch_horizontal_angle_interval + random_angle_range*(random.random()*2-1)
580                        x = -np.sin(branch_vertical_angle)*np.cos(horizontal_angle)
581                        z = np.sin(branch_vertical_angle)*np.sin(horizontal_angle)
582                        y = np.cos(branch_vertical_angle)
583                        ray_origins = np.array([[0, branch_height, 0]])
584                        ray_directions = np.array([[x, y, z]])
585                        locations, index_ray, index_tri = crown_mesh.ray.intersects_location(
586                            ray_origins=ray_origins,
587                            ray_directions=ray_directions)
588                        dist = np.linalg.norm(locations[0] - ray_origins[0])
589                        if self.clumping_factor < 1:
590                            point_seg = int(dist/0.01)
591                            for k in range(int(point_seg*self.leaf_distance_factor_to_branch_root), point_seg, 1):
592                                # offset to remove h_of_first_branch to match with leaf all_pos
593                                branch_points.append(ray_origins[0] + (k/point_seg)*dist*ray_directions[0]-np.array([0, h_of_first_branch, 0]))
594                                if self.generate_crown_only:
595                                    branch_ids.append(branch_index)
596                        mesh = trimesh.creation.cone(1, 1)
597                        mesh.vertices[:, [1, 2]] = mesh.vertices[:, [2, 1]]
598                        mesh.faces[:, [1, 2]] = mesh.faces[:, [2, 1]]
599                        mesh.apply_scale(
600                            [self.branch_radius_scale * h_radius, dist, self.branch_radius_scale * h_radius])
601                        branch_surface_area += (mesh.area-math.pi*math.pow(self.branch_radius_scale * h_radius, 2))
602                        branch_volume += mesh.volume
603                        mesh.faces += trunk_start_index
604                        trunk_start_index += len(mesh.vertices)
605                        R1 = trimesh.transformations.rotation_matrix(branch_vertical_angle, [0, 0, 1])
606                        R2 = trimesh.transformations.rotation_matrix(horizontal_angle, [0, 1, 0])
607                        R = trimesh.transformations.concatenate_matrices(R2, R1)
608                        mesh.apply_transform(R)
609                        center_pt = np.array([0, branch_height, 0])
610                        mesh.apply_translation(center_pt)
611                        obj = trimesh.exchange.obj.export_obj(mesh)
612                        f_out.write("\n"+obj)
613                        branch_index += 1
614                    index += 1
615            trunk_surface = self.get_trunk_surface_area()
616            trunk_volume = self.get_trunk_volume()
617            self.total_wood_area = trunk_surface + branch_surface_area
618            print("Total wood surface area: %.4f" % (trunk_surface + branch_surface_area))
619            print(" - Total branch surface area: %.4f" % branch_surface_area)
620            print(" - Total trunk surface area: %.4f" % trunk_surface)
621            print("Total wood volume: %.4f" % (branch_volume + trunk_volume))
622            print(" - Total trunk volume: %.4f" % trunk_volume)
623            print(" - Total branch volume: %.4f" % branch_volume)
624
625        if not self.generate_crown_only:
626            self.__compute_leaf_numbers()
627            all_pos = self._generate_leaf_pos()
628            if len(all_pos) > 0:
629                if self.clumping_factor < 1 and self.has_branches and self.has_trunk:
630                    kdtree = cKDTree(branch_points)
631                    near_dists, p_idxs = kdtree.query(all_pos, k=1)
632                    for i in range(len(p_idxs)):
633                        p_idx = p_idxs[i]
634                        branch_point = branch_points[p_idx]
635                        leaf_point = np.array(all_pos[i])
636                        # move the leaf towards the branch
637                        vec_dir = branch_point - leaf_point
638                        dist = np.linalg.norm(vec_dir)
639                        vec_dir = vec_dir/dist
640                        new_leaf_point = leaf_point + vec_dir*dist*(1-self.clumping_factor)
641                        all_pos[i] = new_leaf_point
642
643                all_normal = self._generate_leaf_normal()
644                f_out.write("\ng leaves\n")
645                leaf_length = self._calculate_leaf_length()
646                for i in range(self._num_of_leaves):
647                    leaf_pos = all_pos[i]
648                    leaf_normal = all_normal[i]
649                    points = self._generate_single_leaf(leaf_pos, leaf_normal, leaf_length)
650                    if i % 10000 == 0:
651                        f_out.flush()
652                    if self.leaf_shape == LeafShape.SQUARE:
653                        p1, p2, p3, p4 = points
654                        f_out.write("v %.4f %.4f %.4f\n" % (p1[0], p1[1] + h_of_first_branch, p1[2]))
655                        f_out.write("v %.4f %.4f %.4f\n" % (p2[0], p2[1] + h_of_first_branch, p2[2]))
656                        f_out.write("v %.4f %.4f %.4f\n" % (p3[0], p3[1] + h_of_first_branch, p3[2]))
657                        f_out.write("v %.4f %.4f %.4f\n" % (p4[0], p4[1] + h_of_first_branch, p4[2]))
658                        f_out.write(
659                            "f %d %d %d %d\n" % (trunk_start_index+1, trunk_start_index + 2, trunk_start_index + 3, trunk_start_index + 4))
660                        trunk_start_index += 4
661                    if self.leaf_shape == LeafShape.DISK:
662                        points.append(leaf_pos)
663                        tot_pt_each_leaf = len(points)
664                        for j in range(0, len(points)):
665                            f_out.write("v %.4f %.4f %.4f\n" % (points[j][0], points[j][1] + h_of_first_branch, points[j][2]))
666                        for j in range(0, self.leaf_num_triangles - 1):
667                            f_out.write(
668                                "f %d %d %d\n" % (trunk_start_index+ j + 1,
669                                                  trunk_start_index+ j + 2,
670                                                  trunk_start_index+ tot_pt_each_leaf))
671                        f_out.write(
672                            "f %d %d %d\n" % (trunk_start_index+self.leaf_num_triangles,
673                                              trunk_start_index + 1,
674                                              trunk_start_index + tot_pt_each_leaf))
675                        trunk_start_index += tot_pt_each_leaf
676        else:  # generate boundary
677            crown_volume = self.get_crown_volume()
678            print("Corwn volume:", "%.4f" % crown_volume)
679            if crown_mesh is None:
680                crown_mesh = self.__generate_crown_boundary(h_of_first_branch)
681            f_out.write("\ng leaf_boundary\n")
682            if self.clumping_factor < 1 and self.has_branches and self.has_trunk:
683                branch_points=branch_points+np.array([0, h_of_first_branch, 0])
684                kdtree = cKDTree(branch_points)
685                num_samples = int(crown_volume * 300)
686                sample_points = trimesh.sample.volume_mesh(crown_mesh, num_samples)
687                near_dists, p_idxs = kdtree.query(sample_points, k=1)
688                sample_points_bran_idxs=np.zeros(sample_points.shape[0], dtype=int)
689                for i in range(len(p_idxs)):
690                    p_idx = p_idxs[i]
691                    branch_point = branch_points[p_idx]
692                    sample_points_bran_idxs[i] = branch_ids[p_idx]
693                    leaf_point = np.array(sample_points[i])
694                    # move the leaf towards the branch
695                    vec_dir = branch_point - leaf_point
696                    dist = np.linalg.norm(vec_dir)
697                    vec_dir = vec_dir / dist
698                    new_leaf_point = leaf_point + vec_dir * dist * (1 - self.clumping_factor)
699                    sample_points[i] = new_leaf_point
700                tot_vol = 0
701                tmp_dir = tempfile.mkdtemp()
702                alpha_cg = AlphaShapeCGAL(tmp_dir, True)
703                for idx in np.unique(sample_points_bran_idxs):
704                    single_samole_points = sample_points[sample_points_bran_idxs == idx]
705                    # alpha_shape = alphashape.alphashape(single_samole_points, 0.1)
706                    if single_samole_points.shape[0] > 3:
707                        alpha_shape = alpha_cg.alphashape(single_samole_points, 5)
708                        if isinstance(alpha_shape, trimesh.base.Trimesh):
709                            tot_vol += alpha_shape.volume
710                            alpha_shape.faces += trunk_start_index
711                            obj = trimesh.exchange.obj.export_obj(alpha_shape)
712                            f_out.write("\n"+obj)
713                            trunk_start_index += len(alpha_shape.vertices)
714                alpha_cg.free_library()
715                shutil.rmtree(tmp_dir)
716                print("Crown volume after clumping:", "%.4f" % tot_vol)
717                self.clumped_volume = tot_vol
718
719            else:
720                crown_mesh.faces += trunk_start_index
721                obj = trimesh.exchange.obj.export_obj(crown_mesh)
722                f_out.write(obj)
723                trunk_start_index += len(crown_mesh.vertices)
724                self.clumped_volume = crown_volume
725            f_out.close()
726
727
728if __name__ == "__main__":
729    import argparse
730
731    parser = argparse.ArgumentParser()
732    parser.add_argument("--crown_shape", help="Option: Ellipsoid, Cube, Cylinder, Cone, Asymmeric_Gaussian", type=str, default="Ellipsoid")
733    parser.add_argument('--crown_height', help="Unit: m", type=float, default=4)
734    parser.add_argument("--crown_diameter_sn", help="Unit: m", type=float, default=2)
735    parser.add_argument("--crown_diameter_ew", help="Unit: m", type=float, default=2)
736    parser.add_argument("--trunk_height", help="Unit: m", type=float, default=3)
737    parser.add_argument("--dbh", help="Diameter at breast height, Unit: m", type=float, default="0.2")
738    parser.add_argument("--lad", help="Option:Spherical, Uniform, Planophile, Plagiophile, Erectophile, "
739                                      "Extremophile", type=str, default="Spherical")
740    parser.add_argument("--lower_sigma", help="Lower shape of the crown for Asymmeric_Gaussian", type=float, default=0.1)
741    parser.add_argument("--upper_sigma", help="Upper shape of the crown for Asymmeric_Gaussian", type=float,
742                        default=1)
743    parser.add_argument("--gamma_shape", help="Lower shape of the crown for Asymmeric_Gaussian", type=float,
744                        default=1)
745    parser.add_argument("--lvd", help="", type=float, default=1.0)
746    parser.add_argument("--leaf_shape", help="Option: Square, Disk", type=str, default="Square")
747    parser.add_argument("--polygon_sides", help="Only for disk leaf", type=int, default=6)
748    parser.add_argument("--single_leaf_area", help="Unit: Square meter", type=float, default=0.0025)
749    parser.add_argument("--generate_crown_only", help="Generate crown boundary only", action="store_true")
750
751    parser.add_argument("--has_branches", help="Has branches", action="store_true")
752    parser.add_argument("--branch_vertical_angle", help="Angle between branch and trunk", type=float, default=45)
753    parser.add_argument("--vertical_distance_between_branch", help="vertical_distance_between_branch", type=float, default=0.8)
754    parser.add_argument("--num_branches_each_height", help="num_branches_each_height", type=int,
755                        default=3)
756    parser.add_argument("--branch_radius_scale", help="branch_radius_scale", type=float,
757                        default=0.33)
758    parser.add_argument("--random_angle_range", help="Generate a random angle within [-random_angle_range, random_angle_range] to offset angles",
759                        type=float, default=20)
760    parser.add_argument("--clumping_factor", type=float, default=1.0)
761    parser.add_argument("--leaf_distance_factor_to_branch_root", type=float, default=0.3)
762
763    parser.add_argument("--out_obj_path", help="", type=str, required=True)
764    args = parser.parse_args()
765
766    cg = CrownGenerator()
767    cg.crown_shape = args.crown_shape
768    cg.crown_height = args.crown_height
769    cg.crown_diameter_SN = args.crown_diameter_sn
770    cg.crown_diameter_EW = args.crown_diameter_ew
771    if args.trunk_height > 0:
772        cg.trunk_height = args.trunk_height
773        cg.has_trunk = True
774    else:
775        cg.has_trunk = False
776    cg.dbh = args.dbh
777    if cg.dbh <= 0:
778        cg.has_trunk = False
779    cg.leaf_angle_dist = args.lad
780    cg.leaf_volume_density = args.lvd
781    cg.leaf_shape = args.leaf_shape
782    cg.leaf_num_triangles = args.polygon_sides
783    cg.single_leaf_area = args.single_leaf_area
784    cg.sig_lower = args.lower_sigma
785    cg.sig_upper = args.upper_sigma
786    cg.gamma_shape = args.gamma_shape
787    cg.generate_crown_only = args.generate_crown_only
788
789    cg.has_branches = args.has_branches
790    cg.branch_vertical_angle = args.branch_vertical_angle
791    cg.vertical_distance_between_branch = args.vertical_distance_between_branch
792    cg.num_branches_each_height = args.num_branches_each_height
793    cg.branch_radius_scale = args.branch_radius_scale
794    cg.random_angle_range = args.random_angle_range
795    if cg.branch_radius_scale <= 0 or cg.branch_vertical_angle <= 0 or cg.branch_vertical_angle >= 180:
796        cg.has_branches = False
797    cg.clumping_factor = args.clumping_factor
798    cg.leaf_distance_factor_to_branch_root = args.leaf_distance_factor_to_branch_root
799    cg.generate_crown(args.out_obj_path)
800
801    # leaf_nums = [1304,1467,1630,1793,1956,2119,2282,2445,2608,2771,2934,3097,3259,3422,3585]
802    # for i in range(len(leaf_nums)):
803    #     leaf_num = leaf_nums[i]
804    #     cg = CrownGenerator()
805    #     cg.crown_shape = CrownShape.CUBE
806    #     cg.crown_height = 0.4
807    #     cg.crown_diameter_SN = 4
808    #     cg.crown_diameter_EW = 4
809    #     cg.trunk_height = 5 + i*0.4
810    #     cg.has_trunk = True
811    #     cg.dbh = 0.4
812    #     cg.leaf_angle_dist = LAD.SPHERICAL
813    #     cg.num_of_leaves = leaf_num
814    #     cg.leaf_shape = LeafShape.DISK
815    #     cg.leaf_num_triangles = 6
816    #     cg.single_leaf_area = 0.00196350
817    #     cg.generate_crown(r"d:\TMP\facet_layer_"+"%03d"%(i+1)+".obj")
def agd_amp_max(sig_l, sig_r, gamma):
18def agd_amp_max(sig_l, sig_r, gamma):
19    tmp = math.sqrt(math.gamma(1 / gamma) / math.gamma(3 / gamma))
20    beta_l = sig_l * tmp
21    beta_r = sig_r * tmp
22    A = gamma / ((beta_l + beta_r) * math.gamma(1 / gamma))
23    return A
def agd(x, sig_l, sig_r, gamma=2.0, m=0.0):
26def agd(x, sig_l, sig_r, gamma=2.0, m=0.0):
27    """
28    Asymmetric gaussian distribution
29    """
30    tmp = math.sqrt(math.gamma(1 / gamma) / math.gamma(3 / gamma))
31    beta_l = sig_l * tmp
32    beta_r = sig_r * tmp
33    A = gamma / ((beta_l + beta_r) * math.gamma(1 / gamma))
34    if x <= m:
35        y = A * math.exp(-(-(x - m) / beta_l) ** gamma)
36    else:
37        y = A * math.exp(-((x - m) / beta_r) ** gamma)
38    return y

Asymmetric gaussian distribution

class LAD:
41class LAD:
42    UNIFORM = "Uniform"
43    SPHERICAL = "Spherical"
44    ERECTOPHILE = "Erectophile"
45    EXTREMOPHILE = "Extremophile"
46    PLANOPHILE = "Planophile"
47    PLAGIOPHILE = "Plagiophile"
UNIFORM = 'Uniform'
SPHERICAL = 'Spherical'
ERECTOPHILE = 'Erectophile'
EXTREMOPHILE = 'Extremophile'
PLANOPHILE = 'Planophile'
PLAGIOPHILE = 'Plagiophile'
class CrownShape:
50class CrownShape:
51    CUBE = "Cube"
52    ELLIPSOID = "Ellipsoid"
53    CYLINDER = "Cylinder"
54    CONE = "Cone"
55    ASYMMETRIC_GAUSSIAN = "Asymmeric_Gaussian"
56    # USER_DEFINED_CURVE = "USER_DEFINED_CURVE"
CUBE = 'Cube'
ELLIPSOID = 'Ellipsoid'
CYLINDER = 'Cylinder'
CONE = 'Cone'
ASYMMETRIC_GAUSSIAN = 'Asymmeric_Gaussian'
class LeafShape:
59class LeafShape:
60    SQUARE = "Square"
61    DISK = "Disk"
SQUARE = 'Square'
DISK = 'Disk'
class CrownGenerator:
 64class CrownGenerator:
 65    def __init__(self):
 66        self.leaf_angle_dist = LAD.SPHERICAL
 67        self.leaf_volume_density = 1.0
 68        self.crown_diameter_SN = 1
 69        self.crown_diameter_EW = 1
 70        self.crown_height = 3
 71        self.crown_shape = CrownShape.ELLIPSOID
 72        self.leaf_shape = LeafShape.SQUARE
 73        self.leaf_num_triangles = 12
 74        self.single_leaf_area = 0.01
 75
 76        # generate crown boundary only
 77        self.generate_crown_only = False
 78        self.clumped_volume = 0  # If the leaf is turbid, and clumping is less than 1, store the clumped volume
 79        self.total_wood_area = 0
 80
 81        # trunk
 82        self.has_trunk = False
 83        self.trunk_height = 3
 84        self.dbh = 0.2
 85        # This value divides trunk into two segments, for applications such as forest fire severity
 86        self.trunk_division_height = 0
 87
 88        # If generate branches, when trunk is generated
 89        self.has_branches = False
 90        self.branch_vertical_angle = 45  # degree, angle between [0, 1, 0]
 91        self.vertical_distance_between_branch = 0.8  # vertical distance between branches, starting from trunk height
 92        self.num_branches_each_height = 3  # The number of branches at each branch height
 93        self.branch_radius_scale = 0.33  # the scale factor for branch base radius, compared with parent trunk
 94        self.random_angle_range = 20  # the random angle range to offset vertical and horizontal angles
 95
 96        # clumping
 97        # [0, 1] 1 for random, 0 for total clumping
 98        self.clumping_factor = 1.0
 99        # [0, 1) clustering leaf to branch top factor: 1 around branch top, 0 around whole branch
100        self.leaf_distance_factor_to_branch_root = 0.3
101
102        # trunk for asymmetric gaussian
103        self.sig_upper = 2
104        self.sig_lower = 4
105        self.gamma_shape = 6
106
107        # curve for user defined, [(Height, Crown_Radius)]
108        # self.user_curve = [(0, 0.4), (0.2, 0.5)]
109
110        self._num_of_leaves = 100
111        self._num_sigma = 2.0
112
113    def get_crown_volume(self):
114        volume = 0.0
115        if self.crown_shape == CrownShape.CUBE:
116            volume = self.crown_diameter_SN * self.crown_diameter_EW * self.crown_height
117        elif self.crown_shape == CrownShape.ELLIPSOID:
118            volume = 4 * math.pi * (0.5 * self.crown_diameter_EW) * (0.5 * self.crown_diameter_SN) * (
119                        0.5 * self.crown_height) / 3.0
120        elif self.crown_shape == CrownShape.CYLINDER:
121            volume = math.pi * (0.5 * self.crown_diameter_EW) * (0.5 * self.crown_diameter_SN) * self.crown_height
122        elif self.crown_shape == CrownShape.CONE:
123            volume = math.pi * (0.5 * self.crown_diameter_EW) * (0.5 * self.crown_diameter_SN) * self.crown_height / 3.0
124        elif self.crown_shape == CrownShape.ASYMMETRIC_GAUSSIAN:
125            num_sigma = self._num_sigma
126            n_bins = 499
127            x = np.linspace(-num_sigma * self.sig_lower, num_sigma * self.sig_upper, n_bins + 1)
128            step = self.crown_height / n_bins
129            amp_max = agd_amp_max(self.sig_lower, self.sig_upper, self.gamma_shape)
130            y = []
131            for xx in x:
132                yy = agd(xx, self.sig_lower, self.sig_upper, gamma=self.gamma_shape)
133                y.append(self.crown_diameter_SN * 0.5 * yy / amp_max)
134            for i in range(len(x) - 1):
135                s = (1.0 / 3.0) * math.pi * step * (y[i] ** 2 + y[i + 1] ** 2 + y[i] * y[i + 1])
136                volume += s
137        return volume
138
139    def __tree_var_to_asymmetric_gaussian_xy(self, h, dist_trunk):
140        """
141        This function compute the x value, given the height value in the crown
142        """
143        num_sigma = self._num_sigma
144        min_x, max_x = -num_sigma * self.sig_lower, num_sigma * self.sig_upper
145        x = min_x + (max_x - min_x) * (h / self.crown_height)
146        amp_max = agd_amp_max(self.sig_lower, self.sig_upper, self.gamma_shape)
147        y = dist_trunk / (0.5 * self.crown_diameter_SN) * amp_max
148        return x, y, amp_max
149
150    # considering trunk, if leaf position is within radius, it should be regenerated.
151    def consider_by_trunk_radius(self, x, y, z, leaf_length):
152        if not self.has_trunk:
153            return [x, y, z]  # if no trunk, directly return the original xyz
154        else:
155            h = self.trunk_height + y
156            trunk_radius = self.get_trunk_radius_at_height(h)
157            dist_to_center = np.sqrt(x * x + z * z)
158            while dist_to_center < trunk_radius + leaf_length:
159                if self.crown_shape == CrownShape.CUBE:
160                    x = -0.5 * self.crown_diameter_EW + np.random.rand() * self.crown_diameter_EW
161                    z = -0.5 * self.crown_diameter_SN + np.random.rand() * self.crown_diameter_SN
162                    y = np.random.rand() * self.crown_height
163                elif self.crown_shape == CrownShape.ELLIPSOID:
164                    phi = np.random.rand() * np.pi * 2  # randomly azimuth angle
165                    theta = np.arccos(2 * np.random.rand() - 1)  # randomly zenith angle
166                    r = np.cbrt(np.random.rand())
167                    x = r * self.crown_diameter_EW * 0.5 * np.sin(theta) * np.cos(phi)
168                    z = r * self.crown_diameter_SN * 0.5 * np.sin(theta) * np.sin(phi)
169                    y = r * self.crown_height * 0.5 * np.cos(theta) + self.crown_height * 0.5
170                elif self.crown_shape == CrownShape.CYLINDER:
171                    phi = np.random.rand() * np.pi * 2  # randomly azimuth angle
172                    r = np.sqrt(np.random.rand())
173                    x = r * self.crown_diameter_EW * 0.5 * np.cos(phi)
174                    z = r * self.crown_diameter_SN * 0.5 * np.sin(phi)
175                    y = np.random.rand() * self.crown_height
176                elif self.crown_shape == CrownShape.CONE:
177                    phi = np.random.rand() * np.pi * 2  # randomly azimuth angle
178                    h = self.crown_height * (np.random.rand()) ** (1 / 3)
179                    rnd = np.random.rand()
180                    rew = (0.5 * self.crown_diameter_EW / self.crown_height) * h * np.sqrt(rnd)
181                    rsn = (0.5 * self.crown_diameter_SN / self.crown_height) * h * np.sqrt(rnd)
182                    x = rew * np.cos(phi)
183                    z = rsn * np.sin(phi)
184                    y = self.crown_height - h
185                elif self.crown_shape == CrownShape.ASYMMETRIC_GAUSSIAN:
186                    while True:
187                        y = self.crown_height * np.random.rand()
188                        x = -0.5 * self.crown_diameter_EW + np.random.rand() * self.crown_diameter_EW
189                        z = -0.5 * self.crown_diameter_SN + np.random.rand() * self.crown_diameter_SN
190                        dist_to_center1 = np.sqrt(x * x + z * z)
191                        asym_x, asym_y, amp_max = self.__tree_var_to_asymmetric_gaussian_xy(y, dist_to_center1)
192                        pred_y = agd(asym_x, self.sig_lower, self.sig_upper, self.gamma_shape)
193                        if asym_y <= pred_y:
194                            break
195                dist_to_center = np.sqrt(x * x + z * z)
196            return [x, y, z]
197
198    def _generate_leaf_pos(self):
199        leaf_length = self._calculate_leaf_length()
200        all_pos = []
201        if self.crown_shape == CrownShape.CUBE:
202            for i in range(self._num_of_leaves):
203                x = -0.5 * self.crown_diameter_EW + np.random.rand() * self.crown_diameter_EW
204                z = -0.5 * self.crown_diameter_SN + np.random.rand() * self.crown_diameter_SN
205                y = np.random.rand() * self.crown_height
206                all_pos.append(self.consider_by_trunk_radius(x, y, z, leaf_length))
207
208        elif self.crown_shape == CrownShape.ELLIPSOID:
209            for i in range(self._num_of_leaves):
210                phi = np.random.rand() * np.pi * 2  # randomly azimuth angle
211                theta = np.arccos(2 * np.random.rand() - 1)  # randomly zenith angle
212                r = np.cbrt(np.random.rand())
213                x = r * self.crown_diameter_EW * 0.5 * np.sin(theta) * np.cos(phi)
214                z = r * self.crown_diameter_SN * 0.5 * np.sin(theta) * np.sin(phi)
215                y = r * self.crown_height * 0.5 * np.cos(theta) + self.crown_height * 0.5
216                all_pos.append(self.consider_by_trunk_radius(x, y, z, leaf_length))
217        elif self.crown_shape == CrownShape.CYLINDER:
218            for i in range(self._num_of_leaves):
219                phi = np.random.rand() * np.pi * 2  # randomly azimuth angle
220                r = np.sqrt(np.random.rand())
221                x = r * self.crown_diameter_EW * 0.5 * np.cos(phi)
222                z = r * self.crown_diameter_SN * 0.5 * np.sin(phi)
223                y = np.random.rand() * self.crown_height
224                all_pos.append(self.consider_by_trunk_radius(x, y, z, leaf_length))
225        elif self.crown_shape == CrownShape.CONE:
226            for i in range(self._num_of_leaves):
227                phi = np.random.rand() * np.pi * 2  # randomly azimuth angle
228                h = self.crown_height * (np.random.rand()) ** (1 / 3)
229                rnd = np.random.rand()
230                rew = (0.5 * self.crown_diameter_EW / self.crown_height) * h * np.sqrt(rnd)
231                rsn = (0.5 * self.crown_diameter_SN / self.crown_height) * h * np.sqrt(rnd)
232                x = rew * np.cos(phi)
233                z = rsn * np.sin(phi)
234                y = self.crown_height - h
235                all_pos.append(self.consider_by_trunk_radius(x, y, z, leaf_length))
236        elif self.crown_shape == CrownShape.ASYMMETRIC_GAUSSIAN:
237            while True:
238                y = self.crown_height * np.random.rand()
239                x = -0.5 * self.crown_diameter_EW + np.random.rand() * self.crown_diameter_EW
240                z = -0.5 * self.crown_diameter_SN + np.random.rand() * self.crown_diameter_SN
241                dist_to_center = np.sqrt(x * x + z * z)
242                asym_x, asym_y, amp_max = self.__tree_var_to_asymmetric_gaussian_xy(y, dist_to_center)
243                pred_y = agd(asym_x, self.sig_lower, self.sig_upper, self.gamma_shape)
244                if asym_y <= pred_y:
245                    all_pos.append(self.consider_by_trunk_radius(x, y, z, leaf_length))
246                if len(all_pos) == self._num_of_leaves:
247                    break
248        # elif self.crown_shape == CrownShape.USER_DEFINED_CURVE:
249        #     while True:
250        #         y = self.crown_height * np.random.rand()
251        #         x = -0.5 * self.crown_diameter_EW + np.random.rand() * self.crown_diameter_EW
252        #         z = -0.5 * self.crown_diameter_SN + np.random.rand() * self.crown_diameter_SN
253        #         dist_to_center = np.sqrt(x * x + z * z)
254        return all_pos
255
256    def _generate_leaf_normal(self):
257        '''
258        Sphrical: 球形状
259        Uniform:统一型
260        Planophile:平面型
261        Erectophile:竖直型
262        Plagiophile:倾斜型
263        Extremophile:极端型
264        '''
265        all_normals = []
266        if self.leaf_angle_dist == LAD.SPHERICAL:
267            for i in range(self._num_of_leaves):
268                phi = np.random.rand() * np.pi * 2  # randomly azimuth angle
269                theta = np.arccos(np.random.rand())  # randomly zenith angle
270                rx = -np.sin(theta) * np.cos(phi)
271                rz = np.sin(theta) * np.sin(phi)
272                ry = np.cos(theta)
273                all_normals.append([rx, ry, rz])
274            # f = open(r"D:\LESS\simulations\LESSMedium\SimpleHorizontalLayerRAMI\HOM03_ERE_scene.def")
275            # for line in f:
276            #     arr = list(map(lambda x: float(x), line.split()))
277            #     all_normals.append([-arr[4], arr[6], arr[5]])
278        if self.leaf_angle_dist == LAD.UNIFORM:
279            for i in range(self._num_of_leaves):
280                phi = np.random.rand() * np.pi * 2  # randomly azimuth angle
281                theta = np.random.rand() * np.pi * 0.5  # randomly zenith angle
282                rx = -np.sin(theta) * np.cos(phi)
283                rz = np.sin(theta) * np.sin(phi)
284                ry = np.cos(theta)
285                all_normals.append([rx, ry, rz])
286        if self.leaf_angle_dist == LAD.PLANOPHILE:
287            while True:
288                theta = np.random.rand() * np.pi * 0.5
289                y = (2 + 2 * np.cos(2 * theta)) / np.pi
290                rnd_y = np.random.rand() * 4 / np.pi
291                if rnd_y <= y:  # accept
292                    phi = np.random.rand() * np.pi * 2  # randomly azimuth angle
293                    rx = -np.sin(theta) * np.cos(phi)
294                    rz = np.sin(theta) * np.sin(phi)
295                    ry = np.cos(theta)
296                    all_normals.append([rx, ry, rz])
297                    if len(all_normals) == self._num_of_leaves:
298                        break
299        if self.leaf_angle_dist == LAD.ERECTOPHILE:  # 竖直型
300            while True:
301                theta = np.random.rand() * np.pi * 0.5
302                y = (2 - 2 * np.cos(2 * theta)) / np.pi
303                rnd_y = np.random.rand() * 4 / np.pi
304                if rnd_y <= y:  # accept
305                    phi = np.random.rand() * np.pi * 2  # randomly azimuth angle
306                    rx = -np.sin(theta) * np.cos(phi)
307                    rz = np.sin(theta) * np.sin(phi)
308                    ry = np.cos(theta)
309                    all_normals.append([rx, ry, rz])
310                    if len(all_normals) == self._num_of_leaves:
311                        break
312        if self.leaf_angle_dist == LAD.PLAGIOPHILE:  # 倾斜型
313            while True:
314                theta = np.random.rand() * np.pi * 0.5
315                y = (2 - 2 * np.cos(4 * theta)) / np.pi
316                rnd_y = np.random.rand() * 4 / np.pi
317                if rnd_y <= y:  # accept
318                    phi = np.random.rand() * np.pi * 2  # randomly azimuth angle
319                    rx = -np.sin(theta) * np.cos(phi)
320                    rz = np.sin(theta) * np.sin(phi)
321                    ry = np.cos(theta)
322                    all_normals.append([rx, ry, rz])
323                    if len(all_normals) == self._num_of_leaves:
324                        break
325        if self.leaf_angle_dist == LAD.EXTREMOPHILE:  # 极端
326            while True:
327                theta = np.random.rand() * np.pi * 0.5
328                y = (2 + 2 * np.cos(4 * theta)) / np.pi
329                rnd_y = np.random.rand() * 4 / np.pi
330                if rnd_y <= y:  # accept
331                    phi = np.random.rand() * np.pi * 2  # randomly azimuth angle
332                    rx = -np.sin(theta) * np.cos(phi)
333                    rz = np.sin(theta) * np.sin(phi)
334                    ry = np.cos(theta)
335                    all_normals.append([rx, ry, rz])
336                    if len(all_normals) == self._num_of_leaves:
337                        break
338        return all_normals
339
340    @staticmethod
341    def _M(axis, theta):
342        return expm(np.cross(np.eye(3), axis / norm(axis) * theta))
343
344    def _generate_single_leaf(self, leaf_pos, leaf_normal, leaf_length):
345        leaf_side_length = leaf_length
346        # generate leaf according to normal
347        if self.leaf_shape == LeafShape.SQUARE:
348            # First, find a arbitrary vector which is not parallel with normal
349            # and do cross product, the resulting vector is in leaf plane
350            arv = np.array([0, 0, 1])
351            v = np.cross(leaf_normal, arv)
352            v = v / np.linalg.norm(v)
353            p1 = leaf_side_length * np.sqrt(2) * 0.5 * v + leaf_pos
354            v = np.cross(leaf_normal, v)
355            p2 = leaf_side_length * np.sqrt(2) * 0.5 * v + leaf_pos
356            v = np.cross(leaf_normal, v)
357            p3 = leaf_side_length * np.sqrt(2) * 0.5 * v + leaf_pos
358            v = np.cross(leaf_normal, v)
359            p4 = leaf_side_length * np.sqrt(2) * 0.5 * v + leaf_pos
360            return [p1, p2, p3, p4]
361        elif self.leaf_shape == LeafShape.DISK:  # leaf_side_length is leaf radius
362            # First, find a arbitrary vector which is not parallel with normal
363            # and do cross product, the resulting vector is in leaf plane
364            pts = []
365            rot_rad = np.pi * 2 / self.leaf_num_triangles
366            arv = np.array([0, 0, 1])
367            v0 = np.cross(leaf_normal, arv)
368            v0 = v0 / np.linalg.norm(v0)
369            p1 = leaf_side_length * v0 + leaf_pos  # 第一个点
370            pts.append(p1)
371            for i in range(0, self.leaf_num_triangles - 1):
372                m0 = CrownGenerator._M(np.array(leaf_normal), rot_rad * (i + 1))
373                newv = np.dot(m0, v0)
374                newv = newv / np.linalg.norm(newv)
375                newp = leaf_side_length * newv + leaf_pos  # 第一个点
376                pts.append(newp)
377            return pts
378        else:
379            print("Leaf shape is not supported.")
380
381    def _calculate_leaf_length(self):
382        if self.leaf_shape == LeafShape.SQUARE:
383            return np.sqrt(self.single_leaf_area)
384        if self.leaf_shape == LeafShape.DISK:
385            theta = np.pi * 2 / self.leaf_num_triangles
386            tri_leaf = self.single_leaf_area / self.leaf_num_triangles
387            leaf_radius = np.sqrt(2 * tri_leaf / np.sin(theta))
388            return leaf_radius
389
390    def __compute_leaf_numbers(self):
391        crown_volume = self.get_crown_volume()
392        tot_area = crown_volume*self.leaf_volume_density
393        self._num_of_leaves = int(tot_area / self.single_leaf_area)
394        print("Crown volume:", "%.4f" % crown_volume)
395        print("Number of leaves:", self._num_of_leaves)
396        sys.stdout.flush()
397
398    def __generate_crown_boundary(self, h_of_first_branch):
399        mesh = None
400        bound_box = np.array([self.crown_diameter_EW, self.crown_height, self.crown_diameter_SN])
401        center_pt = np.array([0, h_of_first_branch+0.5*self.crown_height, 0])
402        if self.crown_shape == CrownShape.CUBE:
403            mesh = trimesh.creation.box(extents=bound_box)
404            mesh.apply_translation(center_pt)
405        elif self.crown_shape == CrownShape.ELLIPSOID:
406            mesh = trimesh.creation.icosphere()
407            mesh.apply_scale(0.5 * bound_box)
408            mesh.apply_translation(center_pt)
409        elif self.crown_shape == CrownShape.CYLINDER:
410            mesh = trimesh.creation.cylinder(1, 1)
411            mesh.vertices[:, [1, 2]] = mesh.vertices[:, [2, 1]]
412            mesh.faces[:, [1, 2]] = mesh.faces[:, [2, 1]]
413            mesh.apply_scale([0.5 * bound_box[0], bound_box[1], 0.5 * bound_box[2]])
414            mesh.apply_translation(center_pt)
415        elif self.crown_shape == CrownShape.CONE:
416            mesh = trimesh.creation.cone(1, 1)
417            mesh.vertices[:, [1, 2]] = mesh.vertices[:, [2, 1]]
418            mesh.faces[:, [1, 2]] = mesh.faces[:, [2, 1]]
419            mesh.apply_scale([0.5 * bound_box[0], bound_box[1], 0.5 * bound_box[2]])
420            center_pt = np.array([0, h_of_first_branch, 0])
421            mesh.apply_translation(center_pt)
422        elif self.crown_shape == CrownShape.ASYMMETRIC_GAUSSIAN:
423            num_sigma = self._num_sigma
424            n_bins = 199
425            min_bound = -num_sigma * self.sig_lower
426            max_bound = num_sigma * self.sig_upper
427            x_locals = np.linspace(min_bound, max_bound, n_bins + 1)
428            step = self.crown_height / n_bins
429            amp_max = agd_amp_max(self.sig_lower, self.sig_upper, self.gamma_shape)
430            xy = []
431            for x_local in x_locals:
432                y_local = agd(x_local, self.sig_lower, self.sig_upper, gamma=self.gamma_shape)
433                y_global = self.crown_diameter_SN * 0.5 * y_local / amp_max
434                x_global = h_of_first_branch + self.crown_height*(x_local - min_bound) / (max_bound - min_bound)
435                xy.append([y_global, x_global])
436            xy[0][0]=0
437            xy[-1][0]=0
438            mesh = trimesh.creation.revolve(np.array(xy))
439            mesh.vertices[:, [1, 2]] = mesh.vertices[:, [2, 1]]
440            mesh.faces[:, [1, 2]] = mesh.faces[:, [2, 1]]
441        return mesh
442
443    def get_trunk_radius_at_height(self, h):
444        dbh_radis = 0.5 * self.dbh
445        ground_radius = dbh_radis * (self.trunk_height + self.crown_height) / (
446                    self.trunk_height + self.crown_height - 1.3)
447        if self.trunk_height + self.crown_height <= 1.3:
448            ground_radius = dbh_radis
449        division_radius = ground_radius * (self.trunk_height + self.crown_height - h) / (
450                    self.trunk_height + self.crown_height)
451        if division_radius < 0:
452            division_radius = 0
453        return division_radius
454
455    def get_trunk_surface_area(self):
456        dbh_radis = 0.5 * self.dbh
457        ground_radius = dbh_radis * (self.trunk_height + self.crown_height) / (
458                self.trunk_height + self.crown_height - 1.3)
459        if self.trunk_height + self.crown_height <= 1.3:
460            ground_radius = dbh_radis
461        h = self.trunk_height + self.crown_height
462        return math.pi*ground_radius*math.sqrt(h*h+ground_radius*ground_radius)
463
464    def get_trunk_volume(self):
465        dbh_radis = 0.5 * self.dbh
466        ground_radius = dbh_radis * (self.trunk_height + self.crown_height) / (
467                self.trunk_height + self.crown_height - 1.3)
468        if self.trunk_height + self.crown_height <= 1.3:
469            ground_radius = dbh_radis
470        return math.pi*ground_radius*ground_radius*(self.trunk_height + self.crown_height)/3
471
472
473    def generate_crown(self, dist_obj_path):
474        f_out = open(dist_obj_path, "w")
475        trunk_start_index = 0
476        h_of_first_branch = 0
477        if self.has_trunk:
478            h_of_first_branch = self.trunk_height
479        crown_mesh = None
480        # trunk
481        branch_points = []
482        branch_ids = []
483        if self.has_trunk:
484            points_lower = []
485            points_upper = []
486            points_ground = []
487            points_division = []
488            f_out.write("g trunk\n")
489            dbh_radis = 0.5 * self.dbh
490            ground_radius = dbh_radis*(self.trunk_height + self.crown_height)/(self.trunk_height + self.crown_height-1.3)
491            if self.trunk_height + self.crown_height <= 1.3:
492                print("Total tree height is less than 1.3, using DBH as tree base radius.")
493                ground_radius = dbh_radis
494            division_radius = ground_radius*(self.trunk_height + self.crown_height-self.trunk_division_height)/(self.trunk_height + self.crown_height)
495            if self.trunk_height + self.crown_height-self.trunk_division_height <=0:
496                self.trunk_division_height = self.trunk_height + self.crown_height
497                division_radius = 0.001
498
499            for i in range(0, 360, 15):
500                angle_rad = np.radians(i)
501                x_ground = -ground_radius * np.cos(angle_rad)
502                z_ground = ground_radius * np.sin(angle_rad)
503                x_division = -division_radius * np.cos(angle_rad)
504                z_division = division_radius * np.sin(angle_rad)
505                x_top = -0.001 * np.cos(angle_rad)
506                z_top = 0.001 * np.sin(angle_rad)
507                points_lower.append([x_division, self.trunk_division_height, z_division])
508                points_upper.append([x_top, self.trunk_height + self.crown_height, z_top])
509                if self.trunk_division_height > 0:
510                    points_ground.append([x_ground, 0, z_ground])
511                    points_division.append([x_division, self.trunk_division_height, z_division])
512            points_lower.append(points_lower[0])
513            points_upper.append(points_upper[0])
514            if self.trunk_division_height > 0:
515                points_ground.append(points_ground[0])
516                points_division.append(points_division[0])
517            for i in range(0, 24):  # 12个面元
518                f_out.write("v %.4f %.4f %.4f\n" % (points_lower[i][0], points_lower[i][1], points_lower[i][2]))
519                f_out.write(
520                    "v %.4f %.4f %.4f\n" % (points_lower[i + 1][0], points_lower[i + 1][1], points_lower[i + 1][2]))
521                f_out.write(
522                    "v %.4f %.4f %.4f\n" % (points_upper[i + 1][0], points_upper[i + 1][1], points_upper[i + 1][2]))
523                f_out.write("v %.4f %.4f %.4f\n" % (points_upper[i][0], points_upper[i][1], points_upper[i][2]))
524                f_out.write(
525                    "f %d %d %d %d\n" % (trunk_start_index + 1,
526                                         trunk_start_index + 2,
527                                         trunk_start_index + 3,
528                                         trunk_start_index + 4))
529                trunk_start_index += 4
530            #  trunk top
531            for i in range(0, 24):  # 12 top triangles
532                f_out.write(
533                    "v %.4f %.4f %.4f\n" % (points_upper[i][0], points_upper[i][1], points_upper[i][2]))
534                f_out.write(
535                    "v %.4f %.4f %.4f\n" % (points_upper[i + 1][0], points_upper[i + 1][1], points_upper[i + 1][2]))
536                f_out.write(
537                    "v %.4f %.4f %.4f\n" % (0, self.trunk_height, 0))
538                f_out.write(
539                    "f %d %d %d\n" % (trunk_start_index + 1,
540                                      trunk_start_index + 2,
541                                      trunk_start_index + 3))
542                trunk_start_index += 3
543
544            if self.trunk_division_height > 0:
545                f_out.write("\ng trunk_bottom\n")
546                for i in range(0, 24):  # 12个面元
547                    f_out.write("v %.4f %.4f %.4f\n" % (points_ground[i][0], points_ground[i][1], points_ground[i][2]))
548                    f_out.write(
549                        "v %.4f %.4f %.4f\n" % (points_ground[i + 1][0], points_ground[i + 1][1], points_ground[i + 1][2]))
550                    f_out.write(
551                        "v %.4f %.4f %.4f\n" % (points_division[i + 1][0], points_division[i + 1][1], points_division[i + 1][2]))
552                    f_out.write("v %.4f %.4f %.4f\n" % (points_division[i][0], points_division[i][1], points_division[i][2]))
553                    f_out.write(
554                        "f %d %d %d %d\n" % (trunk_start_index + 1,
555                                             trunk_start_index + 2,
556                                             trunk_start_index + 3,
557                                             trunk_start_index + 4))
558                    trunk_start_index += 4
559
560            branch_surface_area = 0
561            branch_volume = 0
562            if self.has_branches:
563                f_out.write("\ng branch\n")
564                if crown_mesh is None:
565                    crown_mesh = self.__generate_crown_boundary(h_of_first_branch)
566                branch_vertical_angle_center = self.branch_vertical_angle/180*math.pi
567                branch_horizontal_angle_interval = 2*math.pi/self.num_branches_each_height
568                num_branch_heights = int(self.crown_height/self.vertical_distance_between_branch)
569                index = 0
570                branch_index = 0
571                for i in range(num_branch_heights):
572                    branch_height = h_of_first_branch + i*self.vertical_distance_between_branch + 0.0001
573                    h_radius = self.get_trunk_radius_at_height(branch_height)
574                    angle_start = 0
575                    if index % 2 == 1:
576                        angle_start = 0.5 * branch_horizontal_angle_interval
577                    for j in range(self.num_branches_each_height):  # for each branch
578                        random_angle_range = self.random_angle_range/180*math.pi
579                        branch_vertical_angle = branch_vertical_angle_center + random_angle_range*(random.random()*2-1)
580                        horizontal_angle = angle_start + j*branch_horizontal_angle_interval + random_angle_range*(random.random()*2-1)
581                        x = -np.sin(branch_vertical_angle)*np.cos(horizontal_angle)
582                        z = np.sin(branch_vertical_angle)*np.sin(horizontal_angle)
583                        y = np.cos(branch_vertical_angle)
584                        ray_origins = np.array([[0, branch_height, 0]])
585                        ray_directions = np.array([[x, y, z]])
586                        locations, index_ray, index_tri = crown_mesh.ray.intersects_location(
587                            ray_origins=ray_origins,
588                            ray_directions=ray_directions)
589                        dist = np.linalg.norm(locations[0] - ray_origins[0])
590                        if self.clumping_factor < 1:
591                            point_seg = int(dist/0.01)
592                            for k in range(int(point_seg*self.leaf_distance_factor_to_branch_root), point_seg, 1):
593                                # offset to remove h_of_first_branch to match with leaf all_pos
594                                branch_points.append(ray_origins[0] + (k/point_seg)*dist*ray_directions[0]-np.array([0, h_of_first_branch, 0]))
595                                if self.generate_crown_only:
596                                    branch_ids.append(branch_index)
597                        mesh = trimesh.creation.cone(1, 1)
598                        mesh.vertices[:, [1, 2]] = mesh.vertices[:, [2, 1]]
599                        mesh.faces[:, [1, 2]] = mesh.faces[:, [2, 1]]
600                        mesh.apply_scale(
601                            [self.branch_radius_scale * h_radius, dist, self.branch_radius_scale * h_radius])
602                        branch_surface_area += (mesh.area-math.pi*math.pow(self.branch_radius_scale * h_radius, 2))
603                        branch_volume += mesh.volume
604                        mesh.faces += trunk_start_index
605                        trunk_start_index += len(mesh.vertices)
606                        R1 = trimesh.transformations.rotation_matrix(branch_vertical_angle, [0, 0, 1])
607                        R2 = trimesh.transformations.rotation_matrix(horizontal_angle, [0, 1, 0])
608                        R = trimesh.transformations.concatenate_matrices(R2, R1)
609                        mesh.apply_transform(R)
610                        center_pt = np.array([0, branch_height, 0])
611                        mesh.apply_translation(center_pt)
612                        obj = trimesh.exchange.obj.export_obj(mesh)
613                        f_out.write("\n"+obj)
614                        branch_index += 1
615                    index += 1
616            trunk_surface = self.get_trunk_surface_area()
617            trunk_volume = self.get_trunk_volume()
618            self.total_wood_area = trunk_surface + branch_surface_area
619            print("Total wood surface area: %.4f" % (trunk_surface + branch_surface_area))
620            print(" - Total branch surface area: %.4f" % branch_surface_area)
621            print(" - Total trunk surface area: %.4f" % trunk_surface)
622            print("Total wood volume: %.4f" % (branch_volume + trunk_volume))
623            print(" - Total trunk volume: %.4f" % trunk_volume)
624            print(" - Total branch volume: %.4f" % branch_volume)
625
626        if not self.generate_crown_only:
627            self.__compute_leaf_numbers()
628            all_pos = self._generate_leaf_pos()
629            if len(all_pos) > 0:
630                if self.clumping_factor < 1 and self.has_branches and self.has_trunk:
631                    kdtree = cKDTree(branch_points)
632                    near_dists, p_idxs = kdtree.query(all_pos, k=1)
633                    for i in range(len(p_idxs)):
634                        p_idx = p_idxs[i]
635                        branch_point = branch_points[p_idx]
636                        leaf_point = np.array(all_pos[i])
637                        # move the leaf towards the branch
638                        vec_dir = branch_point - leaf_point
639                        dist = np.linalg.norm(vec_dir)
640                        vec_dir = vec_dir/dist
641                        new_leaf_point = leaf_point + vec_dir*dist*(1-self.clumping_factor)
642                        all_pos[i] = new_leaf_point
643
644                all_normal = self._generate_leaf_normal()
645                f_out.write("\ng leaves\n")
646                leaf_length = self._calculate_leaf_length()
647                for i in range(self._num_of_leaves):
648                    leaf_pos = all_pos[i]
649                    leaf_normal = all_normal[i]
650                    points = self._generate_single_leaf(leaf_pos, leaf_normal, leaf_length)
651                    if i % 10000 == 0:
652                        f_out.flush()
653                    if self.leaf_shape == LeafShape.SQUARE:
654                        p1, p2, p3, p4 = points
655                        f_out.write("v %.4f %.4f %.4f\n" % (p1[0], p1[1] + h_of_first_branch, p1[2]))
656                        f_out.write("v %.4f %.4f %.4f\n" % (p2[0], p2[1] + h_of_first_branch, p2[2]))
657                        f_out.write("v %.4f %.4f %.4f\n" % (p3[0], p3[1] + h_of_first_branch, p3[2]))
658                        f_out.write("v %.4f %.4f %.4f\n" % (p4[0], p4[1] + h_of_first_branch, p4[2]))
659                        f_out.write(
660                            "f %d %d %d %d\n" % (trunk_start_index+1, trunk_start_index + 2, trunk_start_index + 3, trunk_start_index + 4))
661                        trunk_start_index += 4
662                    if self.leaf_shape == LeafShape.DISK:
663                        points.append(leaf_pos)
664                        tot_pt_each_leaf = len(points)
665                        for j in range(0, len(points)):
666                            f_out.write("v %.4f %.4f %.4f\n" % (points[j][0], points[j][1] + h_of_first_branch, points[j][2]))
667                        for j in range(0, self.leaf_num_triangles - 1):
668                            f_out.write(
669                                "f %d %d %d\n" % (trunk_start_index+ j + 1,
670                                                  trunk_start_index+ j + 2,
671                                                  trunk_start_index+ tot_pt_each_leaf))
672                        f_out.write(
673                            "f %d %d %d\n" % (trunk_start_index+self.leaf_num_triangles,
674                                              trunk_start_index + 1,
675                                              trunk_start_index + tot_pt_each_leaf))
676                        trunk_start_index += tot_pt_each_leaf
677        else:  # generate boundary
678            crown_volume = self.get_crown_volume()
679            print("Corwn volume:", "%.4f" % crown_volume)
680            if crown_mesh is None:
681                crown_mesh = self.__generate_crown_boundary(h_of_first_branch)
682            f_out.write("\ng leaf_boundary\n")
683            if self.clumping_factor < 1 and self.has_branches and self.has_trunk:
684                branch_points=branch_points+np.array([0, h_of_first_branch, 0])
685                kdtree = cKDTree(branch_points)
686                num_samples = int(crown_volume * 300)
687                sample_points = trimesh.sample.volume_mesh(crown_mesh, num_samples)
688                near_dists, p_idxs = kdtree.query(sample_points, k=1)
689                sample_points_bran_idxs=np.zeros(sample_points.shape[0], dtype=int)
690                for i in range(len(p_idxs)):
691                    p_idx = p_idxs[i]
692                    branch_point = branch_points[p_idx]
693                    sample_points_bran_idxs[i] = branch_ids[p_idx]
694                    leaf_point = np.array(sample_points[i])
695                    # move the leaf towards the branch
696                    vec_dir = branch_point - leaf_point
697                    dist = np.linalg.norm(vec_dir)
698                    vec_dir = vec_dir / dist
699                    new_leaf_point = leaf_point + vec_dir * dist * (1 - self.clumping_factor)
700                    sample_points[i] = new_leaf_point
701                tot_vol = 0
702                tmp_dir = tempfile.mkdtemp()
703                alpha_cg = AlphaShapeCGAL(tmp_dir, True)
704                for idx in np.unique(sample_points_bran_idxs):
705                    single_samole_points = sample_points[sample_points_bran_idxs == idx]
706                    # alpha_shape = alphashape.alphashape(single_samole_points, 0.1)
707                    if single_samole_points.shape[0] > 3:
708                        alpha_shape = alpha_cg.alphashape(single_samole_points, 5)
709                        if isinstance(alpha_shape, trimesh.base.Trimesh):
710                            tot_vol += alpha_shape.volume
711                            alpha_shape.faces += trunk_start_index
712                            obj = trimesh.exchange.obj.export_obj(alpha_shape)
713                            f_out.write("\n"+obj)
714                            trunk_start_index += len(alpha_shape.vertices)
715                alpha_cg.free_library()
716                shutil.rmtree(tmp_dir)
717                print("Crown volume after clumping:", "%.4f" % tot_vol)
718                self.clumped_volume = tot_vol
719
720            else:
721                crown_mesh.faces += trunk_start_index
722                obj = trimesh.exchange.obj.export_obj(crown_mesh)
723                f_out.write(obj)
724                trunk_start_index += len(crown_mesh.vertices)
725                self.clumped_volume = crown_volume
726            f_out.close()
leaf_angle_dist
leaf_volume_density
crown_diameter_SN
crown_diameter_EW
crown_height
crown_shape
leaf_shape
leaf_num_triangles
single_leaf_area
generate_crown_only
clumped_volume
total_wood_area
has_trunk
trunk_height
dbh
trunk_division_height
has_branches
branch_vertical_angle
vertical_distance_between_branch
num_branches_each_height
branch_radius_scale
random_angle_range
clumping_factor
leaf_distance_factor_to_branch_root
sig_upper
sig_lower
gamma_shape
def get_crown_volume(self):
113    def get_crown_volume(self):
114        volume = 0.0
115        if self.crown_shape == CrownShape.CUBE:
116            volume = self.crown_diameter_SN * self.crown_diameter_EW * self.crown_height
117        elif self.crown_shape == CrownShape.ELLIPSOID:
118            volume = 4 * math.pi * (0.5 * self.crown_diameter_EW) * (0.5 * self.crown_diameter_SN) * (
119                        0.5 * self.crown_height) / 3.0
120        elif self.crown_shape == CrownShape.CYLINDER:
121            volume = math.pi * (0.5 * self.crown_diameter_EW) * (0.5 * self.crown_diameter_SN) * self.crown_height
122        elif self.crown_shape == CrownShape.CONE:
123            volume = math.pi * (0.5 * self.crown_diameter_EW) * (0.5 * self.crown_diameter_SN) * self.crown_height / 3.0
124        elif self.crown_shape == CrownShape.ASYMMETRIC_GAUSSIAN:
125            num_sigma = self._num_sigma
126            n_bins = 499
127            x = np.linspace(-num_sigma * self.sig_lower, num_sigma * self.sig_upper, n_bins + 1)
128            step = self.crown_height / n_bins
129            amp_max = agd_amp_max(self.sig_lower, self.sig_upper, self.gamma_shape)
130            y = []
131            for xx in x:
132                yy = agd(xx, self.sig_lower, self.sig_upper, gamma=self.gamma_shape)
133                y.append(self.crown_diameter_SN * 0.5 * yy / amp_max)
134            for i in range(len(x) - 1):
135                s = (1.0 / 3.0) * math.pi * step * (y[i] ** 2 + y[i + 1] ** 2 + y[i] * y[i + 1])
136                volume += s
137        return volume
def consider_by_trunk_radius(self, x, y, z, leaf_length):
151    def consider_by_trunk_radius(self, x, y, z, leaf_length):
152        if not self.has_trunk:
153            return [x, y, z]  # if no trunk, directly return the original xyz
154        else:
155            h = self.trunk_height + y
156            trunk_radius = self.get_trunk_radius_at_height(h)
157            dist_to_center = np.sqrt(x * x + z * z)
158            while dist_to_center < trunk_radius + leaf_length:
159                if self.crown_shape == CrownShape.CUBE:
160                    x = -0.5 * self.crown_diameter_EW + np.random.rand() * self.crown_diameter_EW
161                    z = -0.5 * self.crown_diameter_SN + np.random.rand() * self.crown_diameter_SN
162                    y = np.random.rand() * self.crown_height
163                elif self.crown_shape == CrownShape.ELLIPSOID:
164                    phi = np.random.rand() * np.pi * 2  # randomly azimuth angle
165                    theta = np.arccos(2 * np.random.rand() - 1)  # randomly zenith angle
166                    r = np.cbrt(np.random.rand())
167                    x = r * self.crown_diameter_EW * 0.5 * np.sin(theta) * np.cos(phi)
168                    z = r * self.crown_diameter_SN * 0.5 * np.sin(theta) * np.sin(phi)
169                    y = r * self.crown_height * 0.5 * np.cos(theta) + self.crown_height * 0.5
170                elif self.crown_shape == CrownShape.CYLINDER:
171                    phi = np.random.rand() * np.pi * 2  # randomly azimuth angle
172                    r = np.sqrt(np.random.rand())
173                    x = r * self.crown_diameter_EW * 0.5 * np.cos(phi)
174                    z = r * self.crown_diameter_SN * 0.5 * np.sin(phi)
175                    y = np.random.rand() * self.crown_height
176                elif self.crown_shape == CrownShape.CONE:
177                    phi = np.random.rand() * np.pi * 2  # randomly azimuth angle
178                    h = self.crown_height * (np.random.rand()) ** (1 / 3)
179                    rnd = np.random.rand()
180                    rew = (0.5 * self.crown_diameter_EW / self.crown_height) * h * np.sqrt(rnd)
181                    rsn = (0.5 * self.crown_diameter_SN / self.crown_height) * h * np.sqrt(rnd)
182                    x = rew * np.cos(phi)
183                    z = rsn * np.sin(phi)
184                    y = self.crown_height - h
185                elif self.crown_shape == CrownShape.ASYMMETRIC_GAUSSIAN:
186                    while True:
187                        y = self.crown_height * np.random.rand()
188                        x = -0.5 * self.crown_diameter_EW + np.random.rand() * self.crown_diameter_EW
189                        z = -0.5 * self.crown_diameter_SN + np.random.rand() * self.crown_diameter_SN
190                        dist_to_center1 = np.sqrt(x * x + z * z)
191                        asym_x, asym_y, amp_max = self.__tree_var_to_asymmetric_gaussian_xy(y, dist_to_center1)
192                        pred_y = agd(asym_x, self.sig_lower, self.sig_upper, self.gamma_shape)
193                        if asym_y <= pred_y:
194                            break
195                dist_to_center = np.sqrt(x * x + z * z)
196            return [x, y, z]
def get_trunk_radius_at_height(self, h):
443    def get_trunk_radius_at_height(self, h):
444        dbh_radis = 0.5 * self.dbh
445        ground_radius = dbh_radis * (self.trunk_height + self.crown_height) / (
446                    self.trunk_height + self.crown_height - 1.3)
447        if self.trunk_height + self.crown_height <= 1.3:
448            ground_radius = dbh_radis
449        division_radius = ground_radius * (self.trunk_height + self.crown_height - h) / (
450                    self.trunk_height + self.crown_height)
451        if division_radius < 0:
452            division_radius = 0
453        return division_radius
def get_trunk_surface_area(self):
455    def get_trunk_surface_area(self):
456        dbh_radis = 0.5 * self.dbh
457        ground_radius = dbh_radis * (self.trunk_height + self.crown_height) / (
458                self.trunk_height + self.crown_height - 1.3)
459        if self.trunk_height + self.crown_height <= 1.3:
460            ground_radius = dbh_radis
461        h = self.trunk_height + self.crown_height
462        return math.pi*ground_radius*math.sqrt(h*h+ground_radius*ground_radius)
def get_trunk_volume(self):
464    def get_trunk_volume(self):
465        dbh_radis = 0.5 * self.dbh
466        ground_radius = dbh_radis * (self.trunk_height + self.crown_height) / (
467                self.trunk_height + self.crown_height - 1.3)
468        if self.trunk_height + self.crown_height <= 1.3:
469            ground_radius = dbh_radis
470        return math.pi*ground_radius*ground_radius*(self.trunk_height + self.crown_height)/3
def generate_crown(self, dist_obj_path):
473    def generate_crown(self, dist_obj_path):
474        f_out = open(dist_obj_path, "w")
475        trunk_start_index = 0
476        h_of_first_branch = 0
477        if self.has_trunk:
478            h_of_first_branch = self.trunk_height
479        crown_mesh = None
480        # trunk
481        branch_points = []
482        branch_ids = []
483        if self.has_trunk:
484            points_lower = []
485            points_upper = []
486            points_ground = []
487            points_division = []
488            f_out.write("g trunk\n")
489            dbh_radis = 0.5 * self.dbh
490            ground_radius = dbh_radis*(self.trunk_height + self.crown_height)/(self.trunk_height + self.crown_height-1.3)
491            if self.trunk_height + self.crown_height <= 1.3:
492                print("Total tree height is less than 1.3, using DBH as tree base radius.")
493                ground_radius = dbh_radis
494            division_radius = ground_radius*(self.trunk_height + self.crown_height-self.trunk_division_height)/(self.trunk_height + self.crown_height)
495            if self.trunk_height + self.crown_height-self.trunk_division_height <=0:
496                self.trunk_division_height = self.trunk_height + self.crown_height
497                division_radius = 0.001
498
499            for i in range(0, 360, 15):
500                angle_rad = np.radians(i)
501                x_ground = -ground_radius * np.cos(angle_rad)
502                z_ground = ground_radius * np.sin(angle_rad)
503                x_division = -division_radius * np.cos(angle_rad)
504                z_division = division_radius * np.sin(angle_rad)
505                x_top = -0.001 * np.cos(angle_rad)
506                z_top = 0.001 * np.sin(angle_rad)
507                points_lower.append([x_division, self.trunk_division_height, z_division])
508                points_upper.append([x_top, self.trunk_height + self.crown_height, z_top])
509                if self.trunk_division_height > 0:
510                    points_ground.append([x_ground, 0, z_ground])
511                    points_division.append([x_division, self.trunk_division_height, z_division])
512            points_lower.append(points_lower[0])
513            points_upper.append(points_upper[0])
514            if self.trunk_division_height > 0:
515                points_ground.append(points_ground[0])
516                points_division.append(points_division[0])
517            for i in range(0, 24):  # 12个面元
518                f_out.write("v %.4f %.4f %.4f\n" % (points_lower[i][0], points_lower[i][1], points_lower[i][2]))
519                f_out.write(
520                    "v %.4f %.4f %.4f\n" % (points_lower[i + 1][0], points_lower[i + 1][1], points_lower[i + 1][2]))
521                f_out.write(
522                    "v %.4f %.4f %.4f\n" % (points_upper[i + 1][0], points_upper[i + 1][1], points_upper[i + 1][2]))
523                f_out.write("v %.4f %.4f %.4f\n" % (points_upper[i][0], points_upper[i][1], points_upper[i][2]))
524                f_out.write(
525                    "f %d %d %d %d\n" % (trunk_start_index + 1,
526                                         trunk_start_index + 2,
527                                         trunk_start_index + 3,
528                                         trunk_start_index + 4))
529                trunk_start_index += 4
530            #  trunk top
531            for i in range(0, 24):  # 12 top triangles
532                f_out.write(
533                    "v %.4f %.4f %.4f\n" % (points_upper[i][0], points_upper[i][1], points_upper[i][2]))
534                f_out.write(
535                    "v %.4f %.4f %.4f\n" % (points_upper[i + 1][0], points_upper[i + 1][1], points_upper[i + 1][2]))
536                f_out.write(
537                    "v %.4f %.4f %.4f\n" % (0, self.trunk_height, 0))
538                f_out.write(
539                    "f %d %d %d\n" % (trunk_start_index + 1,
540                                      trunk_start_index + 2,
541                                      trunk_start_index + 3))
542                trunk_start_index += 3
543
544            if self.trunk_division_height > 0:
545                f_out.write("\ng trunk_bottom\n")
546                for i in range(0, 24):  # 12个面元
547                    f_out.write("v %.4f %.4f %.4f\n" % (points_ground[i][0], points_ground[i][1], points_ground[i][2]))
548                    f_out.write(
549                        "v %.4f %.4f %.4f\n" % (points_ground[i + 1][0], points_ground[i + 1][1], points_ground[i + 1][2]))
550                    f_out.write(
551                        "v %.4f %.4f %.4f\n" % (points_division[i + 1][0], points_division[i + 1][1], points_division[i + 1][2]))
552                    f_out.write("v %.4f %.4f %.4f\n" % (points_division[i][0], points_division[i][1], points_division[i][2]))
553                    f_out.write(
554                        "f %d %d %d %d\n" % (trunk_start_index + 1,
555                                             trunk_start_index + 2,
556                                             trunk_start_index + 3,
557                                             trunk_start_index + 4))
558                    trunk_start_index += 4
559
560            branch_surface_area = 0
561            branch_volume = 0
562            if self.has_branches:
563                f_out.write("\ng branch\n")
564                if crown_mesh is None:
565                    crown_mesh = self.__generate_crown_boundary(h_of_first_branch)
566                branch_vertical_angle_center = self.branch_vertical_angle/180*math.pi
567                branch_horizontal_angle_interval = 2*math.pi/self.num_branches_each_height
568                num_branch_heights = int(self.crown_height/self.vertical_distance_between_branch)
569                index = 0
570                branch_index = 0
571                for i in range(num_branch_heights):
572                    branch_height = h_of_first_branch + i*self.vertical_distance_between_branch + 0.0001
573                    h_radius = self.get_trunk_radius_at_height(branch_height)
574                    angle_start = 0
575                    if index % 2 == 1:
576                        angle_start = 0.5 * branch_horizontal_angle_interval
577                    for j in range(self.num_branches_each_height):  # for each branch
578                        random_angle_range = self.random_angle_range/180*math.pi
579                        branch_vertical_angle = branch_vertical_angle_center + random_angle_range*(random.random()*2-1)
580                        horizontal_angle = angle_start + j*branch_horizontal_angle_interval + random_angle_range*(random.random()*2-1)
581                        x = -np.sin(branch_vertical_angle)*np.cos(horizontal_angle)
582                        z = np.sin(branch_vertical_angle)*np.sin(horizontal_angle)
583                        y = np.cos(branch_vertical_angle)
584                        ray_origins = np.array([[0, branch_height, 0]])
585                        ray_directions = np.array([[x, y, z]])
586                        locations, index_ray, index_tri = crown_mesh.ray.intersects_location(
587                            ray_origins=ray_origins,
588                            ray_directions=ray_directions)
589                        dist = np.linalg.norm(locations[0] - ray_origins[0])
590                        if self.clumping_factor < 1:
591                            point_seg = int(dist/0.01)
592                            for k in range(int(point_seg*self.leaf_distance_factor_to_branch_root), point_seg, 1):
593                                # offset to remove h_of_first_branch to match with leaf all_pos
594                                branch_points.append(ray_origins[0] + (k/point_seg)*dist*ray_directions[0]-np.array([0, h_of_first_branch, 0]))
595                                if self.generate_crown_only:
596                                    branch_ids.append(branch_index)
597                        mesh = trimesh.creation.cone(1, 1)
598                        mesh.vertices[:, [1, 2]] = mesh.vertices[:, [2, 1]]
599                        mesh.faces[:, [1, 2]] = mesh.faces[:, [2, 1]]
600                        mesh.apply_scale(
601                            [self.branch_radius_scale * h_radius, dist, self.branch_radius_scale * h_radius])
602                        branch_surface_area += (mesh.area-math.pi*math.pow(self.branch_radius_scale * h_radius, 2))
603                        branch_volume += mesh.volume
604                        mesh.faces += trunk_start_index
605                        trunk_start_index += len(mesh.vertices)
606                        R1 = trimesh.transformations.rotation_matrix(branch_vertical_angle, [0, 0, 1])
607                        R2 = trimesh.transformations.rotation_matrix(horizontal_angle, [0, 1, 0])
608                        R = trimesh.transformations.concatenate_matrices(R2, R1)
609                        mesh.apply_transform(R)
610                        center_pt = np.array([0, branch_height, 0])
611                        mesh.apply_translation(center_pt)
612                        obj = trimesh.exchange.obj.export_obj(mesh)
613                        f_out.write("\n"+obj)
614                        branch_index += 1
615                    index += 1
616            trunk_surface = self.get_trunk_surface_area()
617            trunk_volume = self.get_trunk_volume()
618            self.total_wood_area = trunk_surface + branch_surface_area
619            print("Total wood surface area: %.4f" % (trunk_surface + branch_surface_area))
620            print(" - Total branch surface area: %.4f" % branch_surface_area)
621            print(" - Total trunk surface area: %.4f" % trunk_surface)
622            print("Total wood volume: %.4f" % (branch_volume + trunk_volume))
623            print(" - Total trunk volume: %.4f" % trunk_volume)
624            print(" - Total branch volume: %.4f" % branch_volume)
625
626        if not self.generate_crown_only:
627            self.__compute_leaf_numbers()
628            all_pos = self._generate_leaf_pos()
629            if len(all_pos) > 0:
630                if self.clumping_factor < 1 and self.has_branches and self.has_trunk:
631                    kdtree = cKDTree(branch_points)
632                    near_dists, p_idxs = kdtree.query(all_pos, k=1)
633                    for i in range(len(p_idxs)):
634                        p_idx = p_idxs[i]
635                        branch_point = branch_points[p_idx]
636                        leaf_point = np.array(all_pos[i])
637                        # move the leaf towards the branch
638                        vec_dir = branch_point - leaf_point
639                        dist = np.linalg.norm(vec_dir)
640                        vec_dir = vec_dir/dist
641                        new_leaf_point = leaf_point + vec_dir*dist*(1-self.clumping_factor)
642                        all_pos[i] = new_leaf_point
643
644                all_normal = self._generate_leaf_normal()
645                f_out.write("\ng leaves\n")
646                leaf_length = self._calculate_leaf_length()
647                for i in range(self._num_of_leaves):
648                    leaf_pos = all_pos[i]
649                    leaf_normal = all_normal[i]
650                    points = self._generate_single_leaf(leaf_pos, leaf_normal, leaf_length)
651                    if i % 10000 == 0:
652                        f_out.flush()
653                    if self.leaf_shape == LeafShape.SQUARE:
654                        p1, p2, p3, p4 = points
655                        f_out.write("v %.4f %.4f %.4f\n" % (p1[0], p1[1] + h_of_first_branch, p1[2]))
656                        f_out.write("v %.4f %.4f %.4f\n" % (p2[0], p2[1] + h_of_first_branch, p2[2]))
657                        f_out.write("v %.4f %.4f %.4f\n" % (p3[0], p3[1] + h_of_first_branch, p3[2]))
658                        f_out.write("v %.4f %.4f %.4f\n" % (p4[0], p4[1] + h_of_first_branch, p4[2]))
659                        f_out.write(
660                            "f %d %d %d %d\n" % (trunk_start_index+1, trunk_start_index + 2, trunk_start_index + 3, trunk_start_index + 4))
661                        trunk_start_index += 4
662                    if self.leaf_shape == LeafShape.DISK:
663                        points.append(leaf_pos)
664                        tot_pt_each_leaf = len(points)
665                        for j in range(0, len(points)):
666                            f_out.write("v %.4f %.4f %.4f\n" % (points[j][0], points[j][1] + h_of_first_branch, points[j][2]))
667                        for j in range(0, self.leaf_num_triangles - 1):
668                            f_out.write(
669                                "f %d %d %d\n" % (trunk_start_index+ j + 1,
670                                                  trunk_start_index+ j + 2,
671                                                  trunk_start_index+ tot_pt_each_leaf))
672                        f_out.write(
673                            "f %d %d %d\n" % (trunk_start_index+self.leaf_num_triangles,
674                                              trunk_start_index + 1,
675                                              trunk_start_index + tot_pt_each_leaf))
676                        trunk_start_index += tot_pt_each_leaf
677        else:  # generate boundary
678            crown_volume = self.get_crown_volume()
679            print("Corwn volume:", "%.4f" % crown_volume)
680            if crown_mesh is None:
681                crown_mesh = self.__generate_crown_boundary(h_of_first_branch)
682            f_out.write("\ng leaf_boundary\n")
683            if self.clumping_factor < 1 and self.has_branches and self.has_trunk:
684                branch_points=branch_points+np.array([0, h_of_first_branch, 0])
685                kdtree = cKDTree(branch_points)
686                num_samples = int(crown_volume * 300)
687                sample_points = trimesh.sample.volume_mesh(crown_mesh, num_samples)
688                near_dists, p_idxs = kdtree.query(sample_points, k=1)
689                sample_points_bran_idxs=np.zeros(sample_points.shape[0], dtype=int)
690                for i in range(len(p_idxs)):
691                    p_idx = p_idxs[i]
692                    branch_point = branch_points[p_idx]
693                    sample_points_bran_idxs[i] = branch_ids[p_idx]
694                    leaf_point = np.array(sample_points[i])
695                    # move the leaf towards the branch
696                    vec_dir = branch_point - leaf_point
697                    dist = np.linalg.norm(vec_dir)
698                    vec_dir = vec_dir / dist
699                    new_leaf_point = leaf_point + vec_dir * dist * (1 - self.clumping_factor)
700                    sample_points[i] = new_leaf_point
701                tot_vol = 0
702                tmp_dir = tempfile.mkdtemp()
703                alpha_cg = AlphaShapeCGAL(tmp_dir, True)
704                for idx in np.unique(sample_points_bran_idxs):
705                    single_samole_points = sample_points[sample_points_bran_idxs == idx]
706                    # alpha_shape = alphashape.alphashape(single_samole_points, 0.1)
707                    if single_samole_points.shape[0] > 3:
708                        alpha_shape = alpha_cg.alphashape(single_samole_points, 5)
709                        if isinstance(alpha_shape, trimesh.base.Trimesh):
710                            tot_vol += alpha_shape.volume
711                            alpha_shape.faces += trunk_start_index
712                            obj = trimesh.exchange.obj.export_obj(alpha_shape)
713                            f_out.write("\n"+obj)
714                            trunk_start_index += len(alpha_shape.vertices)
715                alpha_cg.free_library()
716                shutil.rmtree(tmp_dir)
717                print("Crown volume after clumping:", "%.4f" % tot_vol)
718                self.clumped_volume = tot_vol
719
720            else:
721                crown_mesh.faces += trunk_start_index
722                obj = trimesh.exchange.obj.export_obj(crown_mesh)
723                f_out.write(obj)
724                trunk_start_index += len(crown_mesh.vertices)
725                self.clumped_volume = crown_volume
726            f_out.close()