BERATools 0.2.0__py3-none-any.whl → 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. beratools/__init__.py +1 -7
  2. beratools/core/algo_centerline.py +491 -351
  3. beratools/core/algo_common.py +497 -0
  4. beratools/core/algo_cost.py +192 -0
  5. beratools/core/{dijkstra_algorithm.py → algo_dijkstra.py} +503 -460
  6. beratools/core/algo_footprint_rel.py +577 -0
  7. beratools/core/algo_line_grouping.py +944 -0
  8. beratools/core/algo_merge_lines.py +214 -0
  9. beratools/core/algo_split_with_lines.py +304 -0
  10. beratools/core/algo_tiler.py +428 -0
  11. beratools/core/algo_vertex_optimization.py +469 -0
  12. beratools/core/constants.py +52 -86
  13. beratools/core/logger.py +76 -85
  14. beratools/core/tool_base.py +196 -133
  15. beratools/gui/__init__.py +11 -15
  16. beratools/gui/{beratools.json → assets/beratools.json} +2185 -2300
  17. beratools/gui/batch_processing_dlg.py +513 -463
  18. beratools/gui/bt_data.py +481 -487
  19. beratools/gui/bt_gui_main.py +710 -691
  20. beratools/gui/main.py +26 -0
  21. beratools/gui/map_window.py +162 -146
  22. beratools/gui/tool_widgets.py +725 -493
  23. beratools/tools/Beratools_r_script.r +1120 -1120
  24. beratools/tools/Ht_metrics.py +116 -116
  25. beratools/tools/__init__.py +7 -7
  26. beratools/tools/batch_processing.py +136 -132
  27. beratools/tools/canopy_threshold_relative.py +672 -670
  28. beratools/tools/canopycostraster.py +222 -222
  29. beratools/tools/centerline.py +136 -176
  30. beratools/tools/common.py +857 -885
  31. beratools/tools/fl_regen_csf.py +428 -428
  32. beratools/tools/forest_line_attributes.py +408 -408
  33. beratools/tools/line_footprint_absolute.py +213 -363
  34. beratools/tools/line_footprint_fixed.py +436 -282
  35. beratools/tools/line_footprint_functions.py +733 -720
  36. beratools/tools/line_footprint_relative.py +73 -64
  37. beratools/tools/line_grouping.py +45 -0
  38. beratools/tools/ln_relative_metrics.py +615 -615
  39. beratools/tools/r_cal_lpi_elai.r +24 -24
  40. beratools/tools/r_generate_pd_focalraster.r +100 -100
  41. beratools/tools/r_interface.py +79 -79
  42. beratools/tools/r_point_density.r +8 -8
  43. beratools/tools/rpy_chm2trees.py +86 -86
  44. beratools/tools/rpy_dsm_chm_by.py +81 -81
  45. beratools/tools/rpy_dtm_by.py +63 -63
  46. beratools/tools/rpy_find_cellsize.py +43 -43
  47. beratools/tools/rpy_gnd_csf.py +74 -74
  48. beratools/tools/rpy_hummock_hollow.py +85 -85
  49. beratools/tools/rpy_hummock_hollow_raster.py +71 -71
  50. beratools/tools/rpy_las_info.py +51 -51
  51. beratools/tools/rpy_laz2las.py +40 -40
  52. beratools/tools/rpy_lpi_elai_lascat.py +466 -466
  53. beratools/tools/rpy_normalized_lidar_by.py +56 -56
  54. beratools/tools/rpy_percent_above_dbh.py +80 -80
  55. beratools/tools/rpy_points2trees.py +88 -88
  56. beratools/tools/rpy_vegcoverage.py +94 -94
  57. beratools/tools/tiler.py +48 -206
  58. beratools/tools/tool_template.py +69 -54
  59. beratools/tools/vertex_optimization.py +61 -620
  60. beratools/tools/zonal_threshold.py +144 -144
  61. beratools-0.2.1.dist-info/METADATA +109 -0
  62. beratools-0.2.1.dist-info/RECORD +74 -0
  63. {beratools-0.2.0.dist-info → beratools-0.2.1.dist-info}/WHEEL +1 -1
  64. {beratools-0.2.0.dist-info → beratools-0.2.1.dist-info}/licenses/LICENSE +22 -22
  65. beratools/gui/cli.py +0 -18
  66. beratools/gui/gui.json +0 -8
  67. beratools/gui_tk/ASCII Banners.txt +0 -248
  68. beratools/gui_tk/__init__.py +0 -20
  69. beratools/gui_tk/beratools_main.py +0 -515
  70. beratools/gui_tk/bt_widgets.py +0 -442
  71. beratools/gui_tk/cli.py +0 -18
  72. beratools/gui_tk/img/BERALogo.png +0 -0
  73. beratools/gui_tk/img/closed.gif +0 -0
  74. beratools/gui_tk/img/closed.png +0 -0
  75. beratools/gui_tk/img/open.gif +0 -0
  76. beratools/gui_tk/img/open.png +0 -0
  77. beratools/gui_tk/img/tool.gif +0 -0
  78. beratools/gui_tk/img/tool.png +0 -0
  79. beratools/gui_tk/main.py +0 -14
  80. beratools/gui_tk/map_window.py +0 -144
  81. beratools/gui_tk/runner.py +0 -1481
  82. beratools/gui_tk/tooltip.py +0 -55
  83. beratools/third_party/pyqtlet2/__init__.py +0 -9
  84. beratools/third_party/pyqtlet2/leaflet/__init__.py +0 -26
  85. beratools/third_party/pyqtlet2/leaflet/control/__init__.py +0 -6
  86. beratools/third_party/pyqtlet2/leaflet/control/control.py +0 -59
  87. beratools/third_party/pyqtlet2/leaflet/control/draw.py +0 -52
  88. beratools/third_party/pyqtlet2/leaflet/control/layers.py +0 -20
  89. beratools/third_party/pyqtlet2/leaflet/core/Parser.py +0 -24
  90. beratools/third_party/pyqtlet2/leaflet/core/__init__.py +0 -2
  91. beratools/third_party/pyqtlet2/leaflet/core/evented.py +0 -180
  92. beratools/third_party/pyqtlet2/leaflet/layer/__init__.py +0 -5
  93. beratools/third_party/pyqtlet2/leaflet/layer/featuregroup.py +0 -34
  94. beratools/third_party/pyqtlet2/leaflet/layer/icon/__init__.py +0 -1
  95. beratools/third_party/pyqtlet2/leaflet/layer/icon/icon.py +0 -30
  96. beratools/third_party/pyqtlet2/leaflet/layer/imageoverlay.py +0 -18
  97. beratools/third_party/pyqtlet2/leaflet/layer/layer.py +0 -105
  98. beratools/third_party/pyqtlet2/leaflet/layer/layergroup.py +0 -45
  99. beratools/third_party/pyqtlet2/leaflet/layer/marker/__init__.py +0 -1
  100. beratools/third_party/pyqtlet2/leaflet/layer/marker/marker.py +0 -91
  101. beratools/third_party/pyqtlet2/leaflet/layer/tile/__init__.py +0 -2
  102. beratools/third_party/pyqtlet2/leaflet/layer/tile/gridlayer.py +0 -4
  103. beratools/third_party/pyqtlet2/leaflet/layer/tile/tilelayer.py +0 -16
  104. beratools/third_party/pyqtlet2/leaflet/layer/vector/__init__.py +0 -5
  105. beratools/third_party/pyqtlet2/leaflet/layer/vector/circle.py +0 -15
  106. beratools/third_party/pyqtlet2/leaflet/layer/vector/circlemarker.py +0 -18
  107. beratools/third_party/pyqtlet2/leaflet/layer/vector/path.py +0 -5
  108. beratools/third_party/pyqtlet2/leaflet/layer/vector/polygon.py +0 -14
  109. beratools/third_party/pyqtlet2/leaflet/layer/vector/polyline.py +0 -18
  110. beratools/third_party/pyqtlet2/leaflet/layer/vector/rectangle.py +0 -14
  111. beratools/third_party/pyqtlet2/leaflet/map/__init__.py +0 -1
  112. beratools/third_party/pyqtlet2/leaflet/map/map.py +0 -220
  113. beratools/third_party/pyqtlet2/mapwidget.py +0 -45
  114. beratools/third_party/pyqtlet2/web/custom.js +0 -43
  115. beratools/third_party/pyqtlet2/web/map.html +0 -23
  116. beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/layers-2x.png +0 -0
  117. beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/layers.png +0 -0
  118. beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/marker-icon-2x.png +0 -0
  119. beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/marker-icon.png +0 -0
  120. beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/marker-shadow.png +0 -0
  121. beratools/third_party/pyqtlet2/web/modules/leaflet_193/leaflet.css +0 -656
  122. beratools/third_party/pyqtlet2/web/modules/leaflet_193/leaflet.js +0 -6
  123. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.codeclimate.yml +0 -14
  124. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.editorconfig +0 -4
  125. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.gitattributes +0 -22
  126. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.travis.yml +0 -43
  127. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/LICENSE +0 -20
  128. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/layers-2x.png +0 -0
  129. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/layers.png +0 -0
  130. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/marker-icon-2x.png +0 -0
  131. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/marker-icon.png +0 -0
  132. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/marker-shadow.png +0 -0
  133. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/spritesheet-2x.png +0 -0
  134. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/spritesheet.png +0 -0
  135. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/spritesheet.svg +0 -156
  136. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/leaflet.draw.css +0 -10
  137. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/leaflet.draw.js +0 -10
  138. beratools/third_party/pyqtlet2/web/modules/leaflet_rotatedMarker_020/LICENSE +0 -22
  139. beratools/third_party/pyqtlet2/web/modules/leaflet_rotatedMarker_020/leaflet.rotatedMarker.js +0 -57
  140. beratools/tools/forest_line_ecosite.py +0 -216
  141. beratools/tools/lapis_all.py +0 -103
  142. beratools/tools/least_cost_path_from_chm.py +0 -152
  143. beratools-0.2.0.dist-info/METADATA +0 -63
  144. beratools-0.2.0.dist-info/RECORD +0 -142
  145. /beratools/gui/{img → assets}/BERALogo.png +0 -0
  146. /beratools/gui/{img → assets}/closed.gif +0 -0
  147. /beratools/gui/{img → assets}/closed.png +0 -0
  148. /beratools/{gui_tk → gui/assets}/gui.json +0 -0
  149. /beratools/gui/{img → assets}/open.gif +0 -0
  150. /beratools/gui/{img → assets}/open.png +0 -0
  151. /beratools/gui/{img → assets}/tool.gif +0 -0
  152. /beratools/gui/{img → assets}/tool.png +0 -0
  153. {beratools-0.2.0.dist-info → beratools-0.2.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,577 @@
1
+ """
2
+ Copyright (C) 2025 Applied Geospatial Research Group.
3
+
4
+ This script is licensed under the GNU General Public License v3.0.
5
+ See <https://gnu.org/licenses/gpl-3.0> for full license details.
6
+
7
+ Author: Richard Zeng, Maverick Fong
8
+
9
+ Description:
10
+ This script is part of the BERA Tools.
11
+ Webpage: https://github.com/appliedgrg/beratools
12
+
13
+ The purpose of this script is to provide main interface for canopy footprint tool.
14
+ The tool is used to generate the footprint of a line based on relative threshold.
15
+ """
16
+ import math
17
+ import time
18
+ from enum import StrEnum
19
+
20
+ import geopandas as gpd
21
+ import numpy as np
22
+ import pandas as pd
23
+ import rasterio.features as ras_feat
24
+ import shapely
25
+ import shapely.geometry as sh_geom
26
+ import shapely.ops as sh_ops
27
+ from skimage.graph import MCP_Flexible
28
+
29
+ import beratools.core.algo_common as algo_common
30
+ import beratools.core.algo_cost as algo_cost
31
+ import beratools.core.constants as bt_const
32
+ import beratools.core.tool_base as bt_base
33
+ import beratools.tools.common as bt_common
34
+
35
+
36
+ class Side(StrEnum):
37
+ """Constants for left and right side."""
38
+
39
+ left = "left"
40
+ right = "right"
41
+
42
+ class FootprintCanopy:
43
+ """Relative canopy footprint class."""
44
+
45
+ def __init__(self, in_geom, in_chm, in_layer=None):
46
+ data = gpd.read_file(in_geom, layer=in_layer)
47
+ self.lines = []
48
+
49
+ for idx in data.index:
50
+ line = LineInfo(data.iloc[[idx]], in_chm)
51
+ self.lines.append(line)
52
+
53
+ def compute(self, processes, parallel_mode=bt_const.ParallelMode.MULTIPROCESSING):
54
+ result = bt_base.execute_multiprocessing(
55
+ algo_common.process_single_item,
56
+ self.lines,
57
+ "Canopy Footprint",
58
+ processes,
59
+ 1,
60
+ parallel_mode,
61
+ )
62
+
63
+ fp = [item.footprint for item in result]
64
+ self.footprints = pd.concat(fp)
65
+
66
+ percentile = [item.line for item in result]
67
+ self.lines_percentile = pd.concat(percentile)
68
+
69
+ def save_footprint(self, out_footprint, layer=None):
70
+ self.footprints.to_file(out_footprint, layer=layer)
71
+
72
+ def save_line_percentile(self, out_percentile):
73
+ self.lines_percentile.to_file(out_percentile)
74
+
75
+ class BufferRing:
76
+ """Buffer ring class."""
77
+
78
+ def __init__(self, ring_poly, side):
79
+ self.geometry = ring_poly
80
+ self.side = side
81
+ self.percentile = 0.5
82
+ self.Dyn_Canopy_Threshold = 0.05
83
+
84
+ class LineInfo:
85
+ """Class to store line information."""
86
+
87
+ def __init__(self,
88
+ line_gdf, in_chm,
89
+ max_ln_width=32,
90
+ tree_radius=1.5,
91
+ max_line_dist=1.5,
92
+ canopy_avoidance=0.0,
93
+ exponent=1.0,
94
+ canopy_thresh_percentage=50):
95
+ self.line = line_gdf
96
+ self.in_chm = in_chm
97
+ self.line_simp = self.line.geometry.simplify(
98
+ tolerance=0.5, preserve_topology=True
99
+ )
100
+
101
+ self.canopy_percentile = 50
102
+ self.DynCanTh = np.nan
103
+ # chk_df_multipart
104
+ # if proc_segments:
105
+ # line_seg = split_into_segments(line_seg)
106
+
107
+ self.buffer_rings = []
108
+
109
+ self.CL_CutHt = np.nan
110
+ self.CR_CutHt = np.nan
111
+ self.RDist_Cut = np.nan
112
+ self.LDist_Cut = np.nan
113
+
114
+ self.canopy_thresh_percentage = canopy_thresh_percentage
115
+ self.canopy_avoidance = canopy_avoidance
116
+ self.exponent = exponent
117
+ self.max_ln_width = max_ln_width
118
+ self.max_line_dist = max_line_dist
119
+ self.tree_radius = tree_radius
120
+
121
+ self.nodata = -9999
122
+ self.dyn_canopy_ndarray = None
123
+ self.negative_cost_clip = None
124
+ self.out_meta = None
125
+
126
+ self.buffer_left = None
127
+ self.buffer_right = None
128
+ self.footprint = None
129
+
130
+ def compute(self):
131
+ self.prepare_ring_buffer()
132
+
133
+ ring_list = []
134
+ for item in self.buffer_rings:
135
+ ring = self.cal_percentileRing(item)
136
+ ring_list.append(ring)
137
+
138
+ self.buffer_rings = ring_list
139
+
140
+ self.rate_of_change(self.get_percentile_array(Side.left), Side.left)
141
+ self.rate_of_change(self.get_percentile_array(Side.right), Side.right)
142
+
143
+ self.line["CL_CutHt"] = self.CL_CutHt
144
+ self.line["CR_CutHt"] = self.CR_CutHt
145
+ self.line["RDist_Cut"] = self.RDist_Cut
146
+ self.line["LDist_Cut"] = self.LDist_Cut
147
+
148
+ self.DynCanTh = (self.CL_CutHt + self.CR_CutHt) / 2
149
+ self.line["DynCanTh"] = self.DynCanTh
150
+
151
+ self.prepare_line_buffer()
152
+
153
+ fp_left = self.process_single_footprint(Side.left)
154
+ fp_right = self.process_single_footprint(Side.right)
155
+ fp_left.geometry = fp_left.buffer(0.005)
156
+ fp_right.geometry = fp_right.buffer(0.005)
157
+ self.footprint = pd.concat([fp_left, fp_right])
158
+ self.footprint = self.footprint.dissolve()
159
+ self.footprint.geometry = self.footprint.buffer(-0.005)
160
+
161
+ # transfer group value to footprint
162
+ # TODO: lines and footprint can match by index
163
+ if bt_const.BT_GROUP in self.line.columns:
164
+ self.footprint[bt_const.BT_GROUP] = self.line[bt_const.BT_GROUP].iloc[0]
165
+
166
+ def prepare_ring_buffer(self):
167
+ nrings = 1
168
+ ringdist = 15
169
+ ring_list = self.multi_ring_buffer(self.line_simp, nrings, ringdist)
170
+ for i in ring_list:
171
+ if BufferRing(i, Side.left):
172
+ self.buffer_rings.append(BufferRing(i, Side.left))
173
+ else:
174
+ print("Empty buffer ring")
175
+
176
+ nrings = -1
177
+ ringdist = -15
178
+ ring_list = self.multi_ring_buffer(self.line_simp, nrings, ringdist)
179
+ for i in ring_list:
180
+ if BufferRing(i, Side.right):
181
+ self.buffer_rings.append(BufferRing(i, Side.right))
182
+ else:
183
+ print("Empty buffer ring")
184
+
185
+ def cal_percentileRing(self, ring):
186
+ try:
187
+ line_buffer = ring.geometry
188
+ if line_buffer.is_empty or shapely.is_missing(line_buffer):
189
+ return None
190
+ if line_buffer.has_z:
191
+ line_buffer = sh_ops.transform(
192
+ lambda x, y, z=None: (x, y), line_buffer
193
+ )
194
+
195
+ except Exception as e:
196
+ print(f"cal_percentileRing: {e}")
197
+
198
+ # TODO: temporary workaround for exception causing not percentile defined
199
+ try:
200
+ clipped_raster, _ = bt_common.clip_raster(self.in_chm, line_buffer, 0)
201
+ clipped_raster = np.squeeze(clipped_raster, axis=0)
202
+
203
+ # mask all -9999 (nodata) value cells
204
+ masked_raster = np.ma.masked_where(
205
+ clipped_raster == bt_const.BT_NODATA, clipped_raster
206
+ )
207
+ filled_raster = np.ma.filled(masked_raster, np.nan)
208
+
209
+ # Calculate the percentile
210
+ percentile = np.nanpercentile(filled_raster, 50)
211
+
212
+ if percentile > 1:
213
+ ring.Dyn_Canopy_Threshold = percentile * (0.3)
214
+ else:
215
+ ring.Dyn_Canopy_Threshold = 1
216
+
217
+ ring.percentile = percentile
218
+ except Exception as e:
219
+ print(e)
220
+ print("Default values are used.")
221
+
222
+ return ring
223
+
224
+ def get_percentile_array(self, side):
225
+ per_array = []
226
+ for item in self.buffer_rings:
227
+ try:
228
+ if item.side == side:
229
+ per_array.append(item.percentile)
230
+ except Exception as e:
231
+ print(e)
232
+
233
+ return per_array
234
+
235
+ def rate_of_change(self, percentile_array, side):
236
+ # Since the x interval is 1 unit, the array 'diff' is the rate of change (slope)
237
+ diff = np.ediff1d(percentile_array)
238
+ cut_dist = len(percentile_array) / 5
239
+
240
+ median_percentile = np.nanmedian(percentile_array)
241
+ if not np.isnan(median_percentile):
242
+ cut_percentile = float(math.floor(median_percentile))
243
+ else:
244
+ cut_percentile = 0.5
245
+
246
+ found = False
247
+ changes = 1.50
248
+ Change = np.insert(diff, 0, 0)
249
+ scale_down = 1.0
250
+
251
+ # test the rate of change is > than 150% (1.5), if it is
252
+ # no result found then lower to 140% (1.4) until 110% (1.1)
253
+ try:
254
+ while not found and changes >= 1.1:
255
+ for ii in range(0, len(Change) - 1):
256
+ if percentile_array[ii] >= 0.5:
257
+ if (Change[ii]) >= changes:
258
+ cut_dist = (ii + 1) * scale_down
259
+ cut_percentile = math.floor(percentile_array[ii])
260
+
261
+ if 0.5 >= cut_percentile:
262
+ if cut_dist > 5:
263
+ cut_percentile = 2
264
+ cut_dist = cut_dist * scale_down**3
265
+ # @<0.5 found and modified
266
+ elif 0.5 < cut_percentile <= 5.0:
267
+ if cut_dist > 6:
268
+ cut_dist = cut_dist * scale_down**3 # 4.0
269
+ # @0.5-5.0 found and modified
270
+ elif 5.0 < cut_percentile <= 10.0:
271
+ if cut_dist > 8: # 5
272
+ cut_dist = cut_dist * scale_down**3
273
+ # @5-10 found and modified
274
+ elif 10.0 < cut_percentile <= 15:
275
+ if cut_dist > 5:
276
+ cut_dist = cut_dist * scale_down**3 # 5.5
277
+ # @10-15 found and modified
278
+ elif 15 < cut_percentile:
279
+ if cut_dist > 4:
280
+ cut_dist = cut_dist * scale_down**2
281
+ cut_percentile = 15.5
282
+ # @>15 found and modified
283
+ found = True
284
+ # rate of change found
285
+ break
286
+ changes = changes - 0.1
287
+
288
+ except IndexError:
289
+ pass
290
+
291
+ # if still no result found, lower to 10% (1.1),
292
+ # if no result found then default is used
293
+ if not found:
294
+ if 0.5 >= median_percentile:
295
+ cut_dist = 4 * scale_down # 3
296
+ cut_percentile = 0.5
297
+ elif 0.5 < median_percentile <= 5.0:
298
+ cut_dist = 4.5 * scale_down # 4.0
299
+ cut_percentile = math.floor(median_percentile)
300
+ elif 5.0 < median_percentile <= 10.0:
301
+ cut_dist = 5.5 * scale_down # 5
302
+ cut_percentile = math.floor(median_percentile)
303
+ elif 10.0 < median_percentile <= 15:
304
+ cut_dist = 6 * scale_down # 5.5
305
+ cut_percentile = math.floor(median_percentile)
306
+ elif 15 < median_percentile:
307
+ cut_dist = 5 * scale_down # 5
308
+ cut_percentile = 15.5
309
+
310
+ if side == Side.right:
311
+ self.RDist_Cut = cut_dist
312
+ self.CR_CutHt = float(cut_percentile)
313
+ elif side == Side.left:
314
+ self.LDist_Cut = cut_dist
315
+ self.CL_CutHt = float(cut_percentile)
316
+
317
+ def multi_ring_buffer(self, df, nrings, ringdist):
318
+ """
319
+ Buffers an input DataFrames geometry nring (number of rings) times.
320
+
321
+ Compute with a distance between rings of ringdist and returns
322
+ a list of non overlapping buffers
323
+ """
324
+ rings = [] # A list to hold the individual buffers
325
+ line = df.geometry.iloc[0]
326
+ # For each ring (1, 2, 3, ..., nrings)
327
+ for ring in np.arange(0, ringdist, nrings):
328
+ big_ring = line.buffer(
329
+ nrings + ring, single_sided=True, cap_style="flat"
330
+ ) # Create one big buffer
331
+ small_ring = line.buffer(
332
+ ring, single_sided=True, cap_style="flat"
333
+ ) # Create one smaller one
334
+ the_ring = big_ring.difference(
335
+ small_ring
336
+ ) # Difference the big with the small to create a ring
337
+ if (
338
+ ~shapely.is_empty(the_ring)
339
+ or ~shapely.is_missing(the_ring)
340
+ or not None
341
+ or ~the_ring.area == 0
342
+ ):
343
+ if isinstance(the_ring, sh_geom.MultiPolygon) or isinstance(
344
+ the_ring, shapely.Polygon
345
+ ):
346
+ rings.append(the_ring) # Append the ring to the rings list
347
+ else:
348
+ if isinstance(the_ring, shapely.GeometryCollection):
349
+ for i in range(0, len(the_ring.geoms)):
350
+ if not isinstance(the_ring.geoms[i], shapely.LineString):
351
+ rings.append(the_ring.geoms[i])
352
+
353
+ return rings # return the list
354
+
355
+ def prepare_line_buffer(self):
356
+ line = self.line.geometry.iloc[0]
357
+ buffer_left_1 = line.buffer(
358
+ distance=self.max_ln_width + 1,
359
+ cap_style=3,
360
+ single_sided=True,
361
+ )
362
+
363
+ buffer_left_2 = line.buffer(
364
+ distance=-1,
365
+ cap_style=3,
366
+ single_sided=True,
367
+ )
368
+
369
+ self.buffer_left = sh_ops.unary_union([buffer_left_1, buffer_left_2])
370
+
371
+ buffer_right_1 = line.buffer(
372
+ distance=-self.max_ln_width - 1,
373
+ cap_style=3,
374
+ single_sided=True,
375
+ )
376
+ buffer_right_2 = line.buffer(distance=1, cap_style=3, single_sided=True)
377
+
378
+ self.buffer_right = sh_ops.unary_union([buffer_right_1, buffer_right_2])
379
+
380
+ def dyn_canopy_cost_raster(self, side):
381
+ in_chm_raster = self.in_chm
382
+ # tree_radius = self.tree_radius
383
+ # max_line_dist = self.max_line_dist
384
+ # canopy_avoid = self.canopy_avoidance
385
+ # exponent = self.exponent
386
+ line_df = self.line
387
+ out_meta = self.out_meta
388
+
389
+ canopy_thresh_percentage = self.canopy_thresh_percentage / 100
390
+
391
+ if side == Side.left:
392
+ canopy_ht_threshold = line_df.CL_CutHt * canopy_thresh_percentage
393
+ Cut_Dist = self.LDist_Cut
394
+ line_buffer = self.buffer_left
395
+ elif side == Side.right:
396
+ canopy_ht_threshold = line_df.CR_CutHt * canopy_thresh_percentage
397
+ Cut_Dist = self.RDist_Cut
398
+ line_buffer = self.buffer_right
399
+ else:
400
+ canopy_ht_threshold = 0.5
401
+
402
+ canopy_ht_threshold = float(canopy_ht_threshold)
403
+ if canopy_ht_threshold <= 0:
404
+ canopy_ht_threshold = 0.5
405
+
406
+ # get the round up integer number for tree search radius
407
+ # tree_radius = float(tree_radius)
408
+ # max_line_dist = float(max_line_dist)
409
+ # canopy_avoid = float(canopy_avoid)
410
+ # cost_raster_exponent = float(exponent)
411
+
412
+ try:
413
+ clipped_rasterC, out_meta = bt_common.clip_raster(
414
+ in_chm_raster, line_buffer, 0
415
+ )
416
+ negative_cost_clip, dyn_canopy_ndarray = algo_cost.cost_raster(
417
+ clipped_rasterC,
418
+ out_meta,
419
+ self.tree_radius,
420
+ canopy_ht_threshold,
421
+ self.max_line_dist,
422
+ self.canopy_avoidance,
423
+ self.exponent,
424
+ )
425
+
426
+ return dyn_canopy_ndarray, negative_cost_clip, out_meta, Cut_Dist
427
+
428
+ except Exception as e:
429
+ print(f"dyn_canopy_cost_raster: {e}")
430
+ return None
431
+
432
+ def process_single_footprint(self, side):
433
+ # this will change segment content, and parameters will be changed
434
+ in_canopy_r, in_cost_r, in_meta, Cut_Dist = self.dyn_canopy_cost_raster(side)
435
+
436
+ if np.isnan(in_canopy_r).all():
437
+ print("Canopy raster empty")
438
+
439
+ if np.isnan(in_cost_r).all():
440
+ print("Cost raster empty")
441
+
442
+ exp_shk_cell = self.exponent # TODO: duplicate vars
443
+ no_data = self.nodata
444
+
445
+ shapefile_proj = self.line.crs
446
+ in_transform = in_meta["transform"]
447
+
448
+ segment_list = []
449
+
450
+ feat = self.line.geometry.iloc[0]
451
+ for coord in feat.coords:
452
+ segment_list.append(coord)
453
+
454
+ cell_size_x = in_transform[0]
455
+ cell_size_y = -in_transform[4]
456
+
457
+ # Work out the corridor from both end of the centerline
458
+ try:
459
+ if len(in_cost_r.shape) > 2:
460
+ in_cost_r = np.squeeze(in_cost_r, axis=0)
461
+
462
+ algo_cost.remove_nan_from_array_refactor(in_cost_r)
463
+ in_cost_r[in_cost_r == no_data] = np.inf
464
+
465
+ # generate 1m interval points along line
466
+ distance_delta = 1
467
+ distances = np.arange(0, feat.length, distance_delta)
468
+ multipoint_along_line = [
469
+ feat.interpolate(distance) for distance in distances
470
+ ]
471
+ multipoint_along_line.append(sh_geom.Point(segment_list[-1]))
472
+ # Rasterize points along line
473
+ rasterized_points_Alongln = ras_feat.rasterize(
474
+ multipoint_along_line,
475
+ out_shape=in_cost_r.shape,
476
+ transform=in_transform,
477
+ fill=0,
478
+ all_touched=True,
479
+ default_value=1,
480
+ )
481
+ points_Alongln = np.transpose(np.nonzero(rasterized_points_Alongln))
482
+
483
+ # Find minimum cost paths through an N-d costs array.
484
+ mcp_flexible1 = MCP_Flexible(
485
+ in_cost_r, sampling=(cell_size_x, cell_size_y), fully_connected=True
486
+ )
487
+ flex_cost_alongLn, flex_back_alongLn = mcp_flexible1.find_costs(
488
+ starts=points_Alongln
489
+ )
490
+
491
+ # Generate corridor
492
+ corridor = flex_cost_alongLn
493
+ corridor = np.ma.masked_invalid(corridor)
494
+
495
+ # Calculate minimum value of corridor raster
496
+ if np.ma.min(corridor) is not None:
497
+ corr_min = float(np.ma.min(corridor))
498
+ else:
499
+ corr_min = 0.5
500
+
501
+ # normalize corridor raster by deducting corr_min
502
+ corridor_norm = corridor - corr_min
503
+
504
+ # Set minimum as zero and save minimum file
505
+ corridor_th_value = Cut_Dist / cell_size_x
506
+ if corridor_th_value < 0: # if no threshold found, use default value
507
+ corridor_th_value = bt_const.FP_CORRIDOR_THRESHOLD / cell_size_x
508
+
509
+ corridor_thresh = np.ma.where(corridor_norm >= corridor_th_value, 1.0, 0.0)
510
+ clean_raster = algo_common.morph_raster(
511
+ corridor_thresh, in_canopy_r, exp_shk_cell, cell_size_x
512
+ )
513
+
514
+ # create mask for non-polygon area
515
+ mask = np.where(clean_raster == 1, True, False)
516
+ if clean_raster.dtype == np.int64:
517
+ clean_raster = clean_raster.astype(np.int32)
518
+
519
+ # Process: ndarray to shapely Polygon
520
+ out_polygon = ras_feat.shapes(
521
+ clean_raster, mask=mask, transform=in_transform
522
+ )
523
+
524
+ # create a shapely MultiPolygon
525
+ multi_polygon = []
526
+ for poly, value in out_polygon:
527
+ multi_polygon.append(sh_geom.shape(poly))
528
+ poly = sh_geom.MultiPolygon(multi_polygon)
529
+
530
+ # create a pandas DataFrame for the FP
531
+ out_data = pd.DataFrame(
532
+ {
533
+ "CorriThresh": [corridor_th_value],
534
+ "geometry": [poly]
535
+ }
536
+ )
537
+ out_gdata = gpd.GeoDataFrame(
538
+ out_data, geometry="geometry", crs=shapefile_proj
539
+ )
540
+
541
+ return out_gdata
542
+
543
+ except Exception as e:
544
+ print("Exception: {}".format(e))
545
+
546
+ def line_footprint_rel(
547
+ in_line,
548
+ in_chm,
549
+ out_footprint,
550
+ processes,
551
+ verbose=True,
552
+ in_layer=None,
553
+ out_layer=None,
554
+ max_ln_width=32,
555
+ tree_radius=1.5,
556
+ max_line_dist=1.5,
557
+ canopy_avoidance=0.0,
558
+ exponent=1.0,
559
+ canopy_thresh_percentage=50,
560
+ parallel_mode=bt_const.ParallelMode.MULTIPROCESSING,
561
+ ):
562
+ """Another version of relative canopy footprint tool."""
563
+ footprint = FootprintCanopy(in_line, in_chm, in_layer)
564
+ footprint.compute(processes, parallel_mode)
565
+
566
+ # footprint.save_line_percentile(out_file_percentile)
567
+ footprint.save_footprint(out_footprint, out_layer)
568
+
569
+ if __name__ == "__main__":
570
+ """This part is to be another version of relative canopy footprint tool."""
571
+ in_args, in_verbose = bt_common.check_arguments()
572
+ start_time = time.time()
573
+ line_footprint_rel(
574
+ **in_args.input, processes=int(in_args.processes), verbose=in_verbose
575
+ )
576
+
577
+ print("Elapsed time: {}".format(time.time() - start_time))