BERATools 0.2.0__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 (142) hide show
  1. beratools/__init__.py +9 -0
  2. beratools/core/__init__.py +0 -0
  3. beratools/core/algo_centerline.py +351 -0
  4. beratools/core/constants.py +86 -0
  5. beratools/core/dijkstra_algorithm.py +460 -0
  6. beratools/core/logger.py +85 -0
  7. beratools/core/tool_base.py +133 -0
  8. beratools/gui/__init__.py +15 -0
  9. beratools/gui/batch_processing_dlg.py +463 -0
  10. beratools/gui/beratools.json +2300 -0
  11. beratools/gui/bt_data.py +487 -0
  12. beratools/gui/bt_gui_main.py +691 -0
  13. beratools/gui/cli.py +18 -0
  14. beratools/gui/gui.json +8 -0
  15. beratools/gui/img/BERALogo.png +0 -0
  16. beratools/gui/img/closed.gif +0 -0
  17. beratools/gui/img/closed.png +0 -0
  18. beratools/gui/img/open.gif +0 -0
  19. beratools/gui/img/open.png +0 -0
  20. beratools/gui/img/tool.gif +0 -0
  21. beratools/gui/img/tool.png +0 -0
  22. beratools/gui/map_window.py +146 -0
  23. beratools/gui/tool_widgets.py +493 -0
  24. beratools/gui_tk/ASCII Banners.txt +248 -0
  25. beratools/gui_tk/__init__.py +20 -0
  26. beratools/gui_tk/beratools_main.py +515 -0
  27. beratools/gui_tk/bt_widgets.py +442 -0
  28. beratools/gui_tk/cli.py +18 -0
  29. beratools/gui_tk/gui.json +8 -0
  30. beratools/gui_tk/img/BERALogo.png +0 -0
  31. beratools/gui_tk/img/closed.gif +0 -0
  32. beratools/gui_tk/img/closed.png +0 -0
  33. beratools/gui_tk/img/open.gif +0 -0
  34. beratools/gui_tk/img/open.png +0 -0
  35. beratools/gui_tk/img/tool.gif +0 -0
  36. beratools/gui_tk/img/tool.png +0 -0
  37. beratools/gui_tk/main.py +14 -0
  38. beratools/gui_tk/map_window.py +144 -0
  39. beratools/gui_tk/runner.py +1481 -0
  40. beratools/gui_tk/tooltip.py +55 -0
  41. beratools/third_party/pyqtlet2/__init__.py +9 -0
  42. beratools/third_party/pyqtlet2/leaflet/__init__.py +26 -0
  43. beratools/third_party/pyqtlet2/leaflet/control/__init__.py +6 -0
  44. beratools/third_party/pyqtlet2/leaflet/control/control.py +59 -0
  45. beratools/third_party/pyqtlet2/leaflet/control/draw.py +52 -0
  46. beratools/third_party/pyqtlet2/leaflet/control/layers.py +20 -0
  47. beratools/third_party/pyqtlet2/leaflet/core/Parser.py +24 -0
  48. beratools/third_party/pyqtlet2/leaflet/core/__init__.py +2 -0
  49. beratools/third_party/pyqtlet2/leaflet/core/evented.py +180 -0
  50. beratools/third_party/pyqtlet2/leaflet/layer/__init__.py +5 -0
  51. beratools/third_party/pyqtlet2/leaflet/layer/featuregroup.py +34 -0
  52. beratools/third_party/pyqtlet2/leaflet/layer/icon/__init__.py +1 -0
  53. beratools/third_party/pyqtlet2/leaflet/layer/icon/icon.py +30 -0
  54. beratools/third_party/pyqtlet2/leaflet/layer/imageoverlay.py +18 -0
  55. beratools/third_party/pyqtlet2/leaflet/layer/layer.py +105 -0
  56. beratools/third_party/pyqtlet2/leaflet/layer/layergroup.py +45 -0
  57. beratools/third_party/pyqtlet2/leaflet/layer/marker/__init__.py +1 -0
  58. beratools/third_party/pyqtlet2/leaflet/layer/marker/marker.py +91 -0
  59. beratools/third_party/pyqtlet2/leaflet/layer/tile/__init__.py +2 -0
  60. beratools/third_party/pyqtlet2/leaflet/layer/tile/gridlayer.py +4 -0
  61. beratools/third_party/pyqtlet2/leaflet/layer/tile/tilelayer.py +16 -0
  62. beratools/third_party/pyqtlet2/leaflet/layer/vector/__init__.py +5 -0
  63. beratools/third_party/pyqtlet2/leaflet/layer/vector/circle.py +15 -0
  64. beratools/third_party/pyqtlet2/leaflet/layer/vector/circlemarker.py +18 -0
  65. beratools/third_party/pyqtlet2/leaflet/layer/vector/path.py +5 -0
  66. beratools/third_party/pyqtlet2/leaflet/layer/vector/polygon.py +14 -0
  67. beratools/third_party/pyqtlet2/leaflet/layer/vector/polyline.py +18 -0
  68. beratools/third_party/pyqtlet2/leaflet/layer/vector/rectangle.py +14 -0
  69. beratools/third_party/pyqtlet2/leaflet/map/__init__.py +1 -0
  70. beratools/third_party/pyqtlet2/leaflet/map/map.py +220 -0
  71. beratools/third_party/pyqtlet2/mapwidget.py +45 -0
  72. beratools/third_party/pyqtlet2/web/custom.js +43 -0
  73. beratools/third_party/pyqtlet2/web/map.html +23 -0
  74. beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/layers-2x.png +0 -0
  75. beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/layers.png +0 -0
  76. beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/marker-icon-2x.png +0 -0
  77. beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/marker-icon.png +0 -0
  78. beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/marker-shadow.png +0 -0
  79. beratools/third_party/pyqtlet2/web/modules/leaflet_193/leaflet.css +656 -0
  80. beratools/third_party/pyqtlet2/web/modules/leaflet_193/leaflet.js +6 -0
  81. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.codeclimate.yml +14 -0
  82. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.editorconfig +4 -0
  83. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.gitattributes +22 -0
  84. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.travis.yml +43 -0
  85. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/LICENSE +20 -0
  86. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/layers-2x.png +0 -0
  87. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/layers.png +0 -0
  88. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/marker-icon-2x.png +0 -0
  89. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/marker-icon.png +0 -0
  90. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/marker-shadow.png +0 -0
  91. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/spritesheet-2x.png +0 -0
  92. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/spritesheet.png +0 -0
  93. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/spritesheet.svg +156 -0
  94. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/leaflet.draw.css +10 -0
  95. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/leaflet.draw.js +10 -0
  96. beratools/third_party/pyqtlet2/web/modules/leaflet_rotatedMarker_020/LICENSE +22 -0
  97. beratools/third_party/pyqtlet2/web/modules/leaflet_rotatedMarker_020/leaflet.rotatedMarker.js +57 -0
  98. beratools/tools/Beratools_r_script.r +1120 -0
  99. beratools/tools/Ht_metrics.py +116 -0
  100. beratools/tools/__init__.py +7 -0
  101. beratools/tools/batch_processing.py +132 -0
  102. beratools/tools/canopy_threshold_relative.py +670 -0
  103. beratools/tools/canopycostraster.py +222 -0
  104. beratools/tools/centerline.py +176 -0
  105. beratools/tools/common.py +885 -0
  106. beratools/tools/fl_regen_csf.py +428 -0
  107. beratools/tools/forest_line_attributes.py +408 -0
  108. beratools/tools/forest_line_ecosite.py +216 -0
  109. beratools/tools/lapis_all.py +103 -0
  110. beratools/tools/least_cost_path_from_chm.py +152 -0
  111. beratools/tools/line_footprint_absolute.py +363 -0
  112. beratools/tools/line_footprint_fixed.py +282 -0
  113. beratools/tools/line_footprint_functions.py +720 -0
  114. beratools/tools/line_footprint_relative.py +64 -0
  115. beratools/tools/ln_relative_metrics.py +615 -0
  116. beratools/tools/r_cal_lpi_elai.r +25 -0
  117. beratools/tools/r_generate_pd_focalraster.r +101 -0
  118. beratools/tools/r_interface.py +80 -0
  119. beratools/tools/r_point_density.r +9 -0
  120. beratools/tools/rpy_chm2trees.py +86 -0
  121. beratools/tools/rpy_dsm_chm_by.py +81 -0
  122. beratools/tools/rpy_dtm_by.py +63 -0
  123. beratools/tools/rpy_find_cellsize.py +43 -0
  124. beratools/tools/rpy_gnd_csf.py +74 -0
  125. beratools/tools/rpy_hummock_hollow.py +85 -0
  126. beratools/tools/rpy_hummock_hollow_raster.py +71 -0
  127. beratools/tools/rpy_las_info.py +51 -0
  128. beratools/tools/rpy_laz2las.py +40 -0
  129. beratools/tools/rpy_lpi_elai_lascat.py +466 -0
  130. beratools/tools/rpy_normalized_lidar_by.py +56 -0
  131. beratools/tools/rpy_percent_above_dbh.py +80 -0
  132. beratools/tools/rpy_points2trees.py +88 -0
  133. beratools/tools/rpy_vegcoverage.py +94 -0
  134. beratools/tools/tiler.py +206 -0
  135. beratools/tools/tool_template.py +54 -0
  136. beratools/tools/vertex_optimization.py +620 -0
  137. beratools/tools/zonal_threshold.py +144 -0
  138. beratools-0.2.0.dist-info/METADATA +63 -0
  139. beratools-0.2.0.dist-info/RECORD +142 -0
  140. beratools-0.2.0.dist-info/WHEEL +4 -0
  141. beratools-0.2.0.dist-info/entry_points.txt +2 -0
  142. beratools-0.2.0.dist-info/licenses/LICENSE +22 -0
@@ -0,0 +1,720 @@
1
+ # ---------------------------------------------------------------------------
2
+ # Copyright (C) 2021 Applied Geospatial Research Group
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, version 3.
7
+ #
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with this program. If not, see <https://gnu.org/licenses/gpl-3.0>.
15
+ #
16
+ # ---------------------------------------------------------------------------
17
+ #
18
+ # Refactor to use for produce dynamic footprint from dynamic canopy and cost raster with open source libraries
19
+ # Prerequisite: Line feature class must have the attribute Fields:"CorridorTh" "DynCanTh" "OLnFID"
20
+ # line_footprint_function.py
21
+ # Maverick Fong
22
+ # Date: 2023-Dec
23
+ # This script is part of the BERA toolset
24
+ # Webpage: https://github.com/
25
+ #
26
+ # Purpose: Creates dynamic footprint polygons for each input line based on a least
27
+ # cost corridor method and individual line thresholds.
28
+ #
29
+ # ---------------------------------------------------------------------------
30
+
31
+ import time
32
+ import numpy as np
33
+
34
+ import rasterio
35
+ from scipy import stats, ndimage
36
+ from geopandas import GeoDataFrame
37
+ from shapely import buffer
38
+ from rasterio import features
39
+ from xrspatial import convolution
40
+
41
+ import skimage
42
+ from skimage.morphology import *
43
+ from skimage.graph import MCP_Flexible
44
+
45
+ from beratools.core.constants import *
46
+ from beratools.core.algo_centerline import *
47
+ from beratools.tools.common import *
48
+
49
+
50
+ def dyn_canopy_cost_raster(args):
51
+ raster_obj = args[0]
52
+ DynCanTh = args[1]
53
+ tree_radius = args[2]
54
+ max_line_dist = args[3]
55
+ canopy_avoid = args[4]
56
+ exponent = args[5]
57
+ res = args[6]
58
+ nodata = args[7]
59
+ line_df = args[8]
60
+ out_meta = args[9]
61
+ line_id = args[10]
62
+ Cut_Dist = args[11]
63
+ Side = args[12]
64
+ canopy_thresh_percentage = float(args[13]) / 100
65
+ line_buffer = args[14]
66
+
67
+ if Side == 'Left':
68
+ canopy_ht_threshold = line_df.CL_CutHt * canopy_thresh_percentage
69
+ elif Side == 'Right':
70
+ canopy_ht_threshold = line_df.CR_CutHt * canopy_thresh_percentage
71
+ elif Side == 'Center':
72
+ canopy_ht_threshold = DynCanTh * canopy_thresh_percentage
73
+ else:
74
+
75
+ canopy_ht_threshold = 0.5
76
+
77
+ canopy_ht_threshold = float(canopy_ht_threshold)
78
+ if canopy_ht_threshold <= 0:
79
+ canopy_ht_threshold = 0.5
80
+ tree_radius = float(tree_radius) # get the round up integer number for tree search radius
81
+ max_line_dist = float(max_line_dist)
82
+ canopy_avoid = float(canopy_avoid)
83
+ cost_raster_exponent = float(exponent)
84
+
85
+ try:
86
+ clipped_rasterC, out_transformC = rasterio.mask.mask(raster_obj, [line_buffer], crop=True,
87
+ filled=False)
88
+
89
+ in_chm = np.squeeze(clipped_rasterC, axis=0)
90
+
91
+ # make rasterio meta for saving raster later
92
+ out_meta = raster_obj.meta.copy()
93
+ out_meta.update({"driver": "GTiff",
94
+ "height": in_chm.shape[0],
95
+ "width": in_chm.shape[1],
96
+ "nodata": BT_NODATA,
97
+ "transform": out_transformC})
98
+
99
+ # print('Loading CHM ...')
100
+ (cell_x, cell_y) = res
101
+ band1_ndarray = in_chm
102
+
103
+ # print('Preparing Kernel window ...')
104
+ kernel = convolution.circle_kernel(cell_x, cell_y, tree_radius)
105
+
106
+ # Generate Canopy Raster and return the Canopy array
107
+ dyn_canopy_ndarray = dyn_np_cc_map(band1_ndarray, canopy_ht_threshold, nodata)
108
+
109
+ # Calculating focal statistic from canopy raster
110
+ cc_std, cc_mean = dyn_fs_raster_stdmean(dyn_canopy_ndarray, kernel, nodata)
111
+
112
+ # Smoothing raster
113
+ cc_smooth = dyn_smooth_cost(dyn_canopy_ndarray, max_line_dist, [cell_x, cell_y])
114
+ avoidance = max(min(float(canopy_avoid), 1), 0)
115
+ dyn_cost_ndarray = dyn_np_cost_raster(dyn_canopy_ndarray, cc_mean, cc_std,
116
+ cc_smooth, avoidance, cost_raster_exponent)
117
+ dyn_cost_ndarray[np.isnan(dyn_cost_ndarray)] = BT_NODATA_COST # TODO was nodata, changed to BT_NODATA_COST
118
+ return (
119
+ line_df, dyn_canopy_ndarray, dyn_cost_ndarray, out_meta,
120
+ max_line_dist, nodata, line_id, Cut_Dist, line_buffer
121
+ )
122
+
123
+ except Exception as e:
124
+ print("Error in dyn_canopy_cost_raster: {}".format(e))
125
+ return None
126
+
127
+
128
+ def split_line_fc(line):
129
+ return list(map(LineString, zip(line.coords[:-1], line.coords[1:])))
130
+
131
+
132
+ def split_line_npart(line):
133
+ # Work out n parts for each line (divided by LP_SEGMENT_LENGTH)
134
+ n = int(np.ceil(line.length / LP_SEGMENT_LENGTH))
135
+ if n > 1:
136
+ # divided line into n-1 equal parts;
137
+ distances = np.linspace(0, line.length, n)
138
+ points = [line.interpolate(distance) for distance in distances]
139
+ line = LineString(points)
140
+ mline = split_line_fc(line)
141
+ else:
142
+ mline = line
143
+ return mline
144
+
145
+
146
+ def split_into_segments(df):
147
+ odf = df
148
+ crs = odf.crs
149
+ if 'OLnSEG' not in odf.columns.array:
150
+ df['OLnSEG'] = np.nan
151
+
152
+ df = odf.assign(geometry=odf.apply(lambda x: split_line_fc(x.geometry), axis=1))
153
+ df = df.explode()
154
+
155
+ df['OLnSEG'] = df.groupby('OLnFID').cumcount()
156
+ gdf = GeoDataFrame(df, geometry=df.geometry, crs=crs)
157
+ gdf = gdf.sort_values(by=['OLnFID', 'OLnSEG'])
158
+ gdf = gdf.reset_index(drop=True)
159
+ return gdf
160
+
161
+
162
+ def split_into_equal_nth_segments(df):
163
+ odf = df
164
+ crs = odf.crs
165
+ if 'OLnSEG' not in odf.columns.array:
166
+ df['OLnSEG'] = np.nan
167
+ df = odf.assign(geometry=odf.apply(lambda x: split_line_npart(x.geometry), axis=1))
168
+ df = df.explode(index_parts=True)
169
+
170
+ df['OLnSEG'] = df.groupby('OLnFID').cumcount()
171
+ gdf = GeoDataFrame(df, geometry=df.geometry, crs=crs)
172
+ gdf = gdf.sort_values(by=['OLnFID', 'OLnSEG'])
173
+ gdf = gdf.reset_index(drop=True)
174
+ return gdf
175
+
176
+
177
+ def generate_line_args(line_seg, work_in_bufferL, work_in_bufferC, raster, tree_radius, max_line_dist,
178
+ canopy_avoidance, exponent, work_in_bufferR, canopy_thresh_percentage):
179
+ line_argsL = []
180
+ line_argsR = []
181
+ line_argsC = []
182
+ line_id = 0
183
+ for record in range(0, len(work_in_bufferL)):
184
+ line_bufferL = work_in_bufferL.loc[record, 'geometry']
185
+ line_bufferC = work_in_bufferC.loc[record, 'geometry']
186
+ LCut = work_in_bufferL.loc[record, 'LDist_Cut']
187
+
188
+ clipped_rasterL, out_transformL = rasterio.mask.mask(raster, [line_bufferL], crop=True,
189
+ nodata=BT_NODATA, filled=True)
190
+ clipped_rasterL = np.squeeze(clipped_rasterL, axis=0)
191
+
192
+ clipped_rasterC, out_transformC = rasterio.mask.mask(raster, [line_bufferC], crop=True,
193
+ nodata=BT_NODATA, filled=True)
194
+
195
+ clipped_rasterC = np.squeeze(clipped_rasterC, axis=0)
196
+
197
+ # make rasterio meta for saving raster later
198
+ out_metaL = raster.meta.copy()
199
+ out_metaL.update({"driver": "GTiff",
200
+ "height": clipped_rasterL.shape[0],
201
+ "width": clipped_rasterL.shape[1],
202
+ "nodata": BT_NODATA,
203
+ "transform": out_transformL})
204
+
205
+ out_metaC = raster.meta.copy()
206
+ out_metaC.update({"driver": "GTiff",
207
+ "height": clipped_rasterC.shape[0],
208
+ "width": clipped_rasterC.shape[1],
209
+ "nodata": BT_NODATA,
210
+ "transform": out_transformC})
211
+
212
+ nodata = BT_NODATA
213
+ line_argsL.append([clipped_rasterL, float(work_in_bufferL.loc[record, 'DynCanTh']), float(tree_radius),
214
+ float(max_line_dist), float(canopy_avoidance), float(exponent), raster.res, nodata,
215
+ line_seg.iloc[[record]], out_metaL, line_id, LCut, 'Left', canopy_thresh_percentage,
216
+ line_bufferC])
217
+
218
+ line_argsC.append([clipped_rasterC, float(work_in_bufferC.loc[record, 'DynCanTh']), float(tree_radius),
219
+ float(max_line_dist), float(canopy_avoidance), float(exponent), raster.res, nodata,
220
+ line_seg.iloc[[record]], out_metaC, line_id, 10, 'Center', canopy_thresh_percentage,
221
+ line_bufferC])
222
+
223
+ line_id += 1
224
+
225
+ line_id = 0
226
+ for record in range(0, len(work_in_bufferR)):
227
+ line_bufferR = work_in_bufferR.loc[record, 'geometry']
228
+ RCut = work_in_bufferR.loc[record, 'RDist_Cut']
229
+ clipped_rasterR, out_transformR = rasterio.mask.mask(raster, [line_bufferR], crop=True,
230
+ nodata=BT_NODATA, filled=True)
231
+ clipped_rasterR = np.squeeze(clipped_rasterR, axis=0)
232
+
233
+ # make rasterio meta for saving raster later
234
+ out_metaR = raster.meta.copy()
235
+ out_metaR.update({"driver": "GTiff",
236
+ "height": clipped_rasterR.shape[0],
237
+ "width": clipped_rasterR.shape[1],
238
+ "nodata": BT_NODATA,
239
+ "transform": out_transformR})
240
+ line_bufferC = work_in_bufferC.loc[record, 'geometry']
241
+ clipped_rasterC, out_transformC = rasterio.mask.mask(raster, [line_bufferC], crop=True,
242
+ nodata=BT_NODATA, filled=True)
243
+
244
+ clipped_rasterC = np.squeeze(clipped_rasterC, axis=0)
245
+ out_metaC = raster.meta.copy()
246
+ out_metaC.update({"driver": "GTiff",
247
+ "height": clipped_rasterC.shape[0],
248
+ "width": clipped_rasterC.shape[1],
249
+ "nodata": BT_NODATA,
250
+ "transform": out_transformC})
251
+
252
+ nodata = BT_NODATA
253
+ # TODO deal with inherited nodata and BT_NODATA_COST
254
+ # TODO convert nodata to BT_NODATA_COST
255
+ line_argsR.append([clipped_rasterR, float(work_in_bufferR.loc[record, 'DynCanTh']), float(tree_radius),
256
+ float(max_line_dist), float(canopy_avoidance), float(exponent), raster.res, nodata,
257
+ line_seg.iloc[[record]], out_metaR, line_id, RCut, 'Right', canopy_thresh_percentage,
258
+ line_bufferC])
259
+
260
+ print(' "PROGRESS_LABEL Preparing... {} of {}" '.format(line_id + 1 + len(work_in_bufferL),
261
+ len(work_in_bufferL) + len(work_in_bufferR)),
262
+ flush=True)
263
+ print(
264
+ ' %{} '.format((line_id + 1 + len(work_in_bufferL)) / (len(work_in_bufferL) + len(work_in_bufferR)) * 100),
265
+ flush=True)
266
+
267
+ line_id += 1
268
+
269
+ return line_argsL, line_argsR, line_argsC
270
+
271
+ #
272
+ # def find_corridor_threshold_boundary(canopy_clip, least_cost_path, corridor_raster):
273
+ # threshold = -1
274
+ # thresholds = [-1] * 10
275
+ #
276
+ # # morphological filters to get polygons from canopy raster
277
+ # canopy_bin = np.where(np.isclose(canopy_clip, 1.0), True, False)
278
+ # clean_holes = remove_small_holes(canopy_bin)
279
+ # clean_obj = remove_small_objects(clean_holes)
280
+ #
281
+ # polys = features.shapes(skimage.img_as_ubyte(clean_obj), mask=clean_obj)
282
+ # polys = [shape(poly).segmentize(FP_SEGMENTIZE_LENGTH) for poly, _ in polys]
283
+ #
284
+ # # perpendicular segments intersections with polygons
285
+ # size = corridor_raster.shape
286
+ # pts = []
287
+ # for poly in polys:
288
+ # pts.extend(list(poly.exterior.coords))
289
+ #
290
+ # index_0 = []
291
+ # index_1 = []
292
+ # for pt in pts:
293
+ # if int(pt[0]) < size[1] and int(pt[1]) < size[0]:
294
+ # index_0.append(int(pt[0]))
295
+ # index_1.append(int(pt[1]))
296
+ #
297
+ # try:
298
+ # thresholds = corridor_raster[index_1, index_0]
299
+ # except Exception as e:
300
+ # print(e)
301
+ #
302
+ # # trimmed mean of values at intersections
303
+ # threshold = stats.trim_mean(thresholds, 0.3)
304
+ #
305
+ # return threshold
306
+
307
+
308
+ def find_corridor_threshold(raster):
309
+ """
310
+ Find the optimal corridor threshold by raster histogram
311
+ Parameters
312
+ ----------
313
+ raster : corridor raster
314
+
315
+ Returns
316
+ -------
317
+ corridor_threshold : float
318
+
319
+ """
320
+ corridor_threshold = -1.0
321
+ hist, bins = np.histogram(raster.flatten(), bins=100, range=(0, 100))
322
+ CostStd = np.nanstd(raster.flatten())
323
+ half_count = np.sum(hist) / 2
324
+ sub_count = 0
325
+
326
+ for count, bin_no in zip(hist, bins):
327
+ sub_count += count
328
+ if sub_count > half_count:
329
+ break
330
+
331
+ corridor_threshold = bin_no
332
+
333
+ return corridor_threshold
334
+
335
+
336
+ def process_single_line_relative(segment):
337
+ in_chm = rasterio.open(segment[0])
338
+ #
339
+ segment[0] = in_chm
340
+ DynCanTh = segment[1]
341
+
342
+ # Segment args from mulitprocessing:
343
+ # [clipped_chm, float(work_in_bufferR.loc[record, 'DynCanTh']), float(tree_radius),
344
+ # float(max_line_dist), float(canopy_avoidance), float(exponent), raster.res, nodata,
345
+ # line_seg.iloc[[record]], out_meta, line_id,RCut,Side,canopy_thresh_percentage,line_buffer]
346
+
347
+ # this will change segment content, and parameters will be changed
348
+ segment = dyn_canopy_cost_raster(segment)
349
+ # Segment after Clipped Canopy and Cost Raster
350
+ # line_df, dyn_canopy_ndarray, dyn_cost_ndarray, out_meta, max_line_dist, nodata, line_id,Cut_Dist,line_buffer
351
+
352
+ # this function takes single line to work the line footprint
353
+ # (regardless it process the whole line or individual segment)
354
+ df = segment[0]
355
+ in_canopy_r = segment[1]
356
+ in_cost_r = segment[2]
357
+ out_meta = segment[3]
358
+
359
+ # in_transform = segment[3]
360
+ if np.isnan(in_canopy_r).all():
361
+ print("Canopy raster empty")
362
+
363
+ if np.isnan(in_cost_r).all():
364
+ print("Cost raster empty")
365
+
366
+ in_meta = segment[3]
367
+ exp_shk_cell = segment[4]
368
+ no_data = segment[5]
369
+ line_id = segment[6]
370
+ Cut_Dist = segment[7]
371
+ line_bufferR = segment[8]
372
+
373
+ shapefile_proj = df.crs
374
+ in_transform = in_meta['transform']
375
+
376
+ FID = df['OLnSEG'] # segment line feature ID
377
+ OID = df['OLnFID'] # original line ID for segment line
378
+
379
+ segment_list = []
380
+
381
+ feat = df.geometry.iloc[0]
382
+ for coord in feat.coords:
383
+ segment_list.append(coord)
384
+
385
+ cell_size_x = in_transform[0]
386
+ cell_size_y = -in_transform[4]
387
+
388
+ # Work out the corridor from both end of the centerline
389
+ try:
390
+ # TODO: further investigate and submit issue to skimage
391
+ # There is a severe bug in skimage find_costs
392
+ # when nan is present in clip_cost_r, find_costs cause access violation
393
+ # no message/exception will be caught
394
+ # change all nan to BT_NODATA_COST for workaround
395
+ remove_nan_from_array(in_cost_r)
396
+
397
+ # generate 1m interval points along line
398
+ distance_delta = 1
399
+ distances = np.arange(0, feat.length, distance_delta)
400
+ multipoint_along_line = [feat.interpolate(distance) for distance in distances]
401
+
402
+ # Rasterize points along line
403
+ rasterized_points_Alongln = features.rasterize(multipoint_along_line, out_shape=in_cost_r.shape,
404
+ transform=in_transform,
405
+ fill=0, all_touched=True, default_value=1)
406
+ points_Alongln = np.transpose(np.nonzero(rasterized_points_Alongln))
407
+
408
+ # Find minimum cost paths through an N-d costs array.
409
+ mcp_flexible1 = MCP_Flexible(in_cost_r, sampling=(cell_size_x, cell_size_y), fully_connected=True)
410
+ flex_cost_alongLn, flex_back_alongLn = mcp_flexible1.find_costs(starts=points_Alongln)
411
+
412
+ # Generate corridor
413
+ # corridor = source_cost_acc + dest_cost_acc
414
+ corridor = flex_cost_alongLn # +flex_cost_dest #cum_cost_tosource+cum_cost_todestination
415
+ corridor = np.ma.masked_invalid(corridor)
416
+
417
+ # Calculate minimum value of corridor raster
418
+ if not np.ma.min(corridor) is None:
419
+ corr_min = float(np.ma.min(corridor))
420
+ else:
421
+ corr_min = 0.5
422
+
423
+ # normalize corridor raster by deducting corr_min
424
+ corridor_norm = corridor - corr_min
425
+
426
+ # Set minimum as zero and save minimum file
427
+ # corridor_th_value = find_corridor_threshold(corridor_norm)
428
+ corridor_th_value = (Cut_Dist / cell_size_x)
429
+ if corridor_th_value < 0: # if no threshold found, use default value
430
+ corridor_th_value = (FP_CORRIDOR_THRESHOLD / cell_size_x)
431
+
432
+ # corridor_th_value = FP_CORRIDOR_THRESHOLD
433
+ corridor_thresh = np.ma.where(corridor_norm >= corridor_th_value, 1.0, 0.0)
434
+ corridor_thresh_cl = np.ma.where(corridor_norm >= (corridor_th_value + (5 / cell_size_x)), 1.0, 0.0)
435
+
436
+ # find contiguous corridor polygon for centerline
437
+ corridor_poly_gpd = find_corridor_polygon(corridor_thresh_cl, in_transform, df)
438
+
439
+ # Process: Stamp CC and Max Line Width
440
+ # Original code here
441
+ # RasterClass = SetNull(IsNull(CorridorMin),((CorridorMin) + ((Canopy_Raster) >= 1)) > 0)
442
+ temp1 = (corridor_thresh + in_canopy_r)
443
+ raster_class = np.ma.where(temp1 == 0, 1, 0).data
444
+
445
+ # BERA proposed Binary morphology
446
+ # RasterClass_binary=np.where(RasterClass==0,False,True)
447
+ if exp_shk_cell > 0 and cell_size_x < 1:
448
+ # Process: Expand
449
+ # FLM original Expand equivalent
450
+ cell_size = int(exp_shk_cell * 2 + 1)
451
+
452
+ expanded = ndimage.grey_dilation(raster_class, size=(cell_size, cell_size))
453
+
454
+ # BERA proposed Binary morphology Expand
455
+ # Expanded = ndimage.binary_dilation(RasterClass_binary, iterations=exp_shk_cell,border_value=1)
456
+
457
+ # Process: Shrink
458
+ # FLM original Shrink equivalent
459
+ file_shrink = ndimage.grey_erosion(expanded, size=(cell_size, cell_size))
460
+
461
+ # BERA proposed Binary morphology Shrink
462
+ # fileShrink = ndimage.binary_erosion((Expanded),iterations=Exp_Shk_cell,border_value=1)
463
+ else:
464
+ print('No Expand And Shrink cell performed.')
465
+
466
+ file_shrink = raster_class
467
+
468
+ # Process: Boundary Clean
469
+ clean_raster = ndimage.gaussian_filter(file_shrink, sigma=0, mode='nearest')
470
+
471
+ # creat mask for non-polygon area
472
+ mask = np.where(clean_raster == 1, True, False)
473
+
474
+ # Process: ndarray to shapely Polygon
475
+ out_polygon = features.shapes(clean_raster, mask=mask, transform=in_transform)
476
+
477
+ # create a shapely multipolygon
478
+ multi_polygon = []
479
+ for poly, value in out_polygon:
480
+ multi_polygon.append(shape(poly))
481
+ poly = MultiPolygon(multi_polygon)
482
+
483
+ # create a pandas dataframe for the FP
484
+ out_data = pd.DataFrame({'OLnFID': OID, 'OLnSEG': FID, 'CorriThresh': corridor_th_value, 'geometry': poly})
485
+ out_gdata = GeoDataFrame(out_data, geometry='geometry', crs=shapefile_proj)
486
+
487
+ return out_gdata, corridor_poly_gpd
488
+
489
+ except Exception as e:
490
+ print('Exception: {}'.format(e))
491
+
492
+
493
+ def multiprocessing_footprint_relative(line_args, processes):
494
+ try:
495
+ total_steps = len(line_args)
496
+
497
+ feats = []
498
+ # chunksize = math.ceil(total_steps / processes)
499
+ with Pool(processes=processes) as pool:
500
+ step = 0
501
+ # execute tasks in order, process results out of order
502
+ for result in pool.imap_unordered(process_single_line_relative, line_args):
503
+ if BT_DEBUGGING:
504
+ print('Got result: {}'.format(result), flush=True)
505
+ feats.append(result)
506
+ step += 1
507
+ print(' "PROGRESS_LABEL Dynamic Segment Line Footprint {} of {}" '.format(step, total_steps),
508
+ flush=True)
509
+ print(' %{} '.format(step / total_steps * 100), flush=True)
510
+ return feats
511
+ except OperationCancelledException:
512
+ print("Operation cancelled")
513
+ return None
514
+
515
+
516
+ def main_line_footprint_relative(callback, in_line, in_chm, max_ln_width, exp_shk_cell, out_footprint, out_centerline,
517
+ tree_radius, max_line_dist, canopy_avoidance, exponent, full_step,
518
+ canopy_thresh_percentage, processes, verbose):
519
+ # use_corridor_th_col = True
520
+ line_seg = GeoDataFrame.from_file(in_line)
521
+
522
+ # If Dynamic canopy threshold column not found, create one
523
+ if 'DynCanTh' not in line_seg.columns.array:
524
+ print("Please create field {} first".format('DynCanTh'))
525
+ exit()
526
+ if not float(canopy_thresh_percentage):
527
+ canopy_thresh_percentage = 50
528
+ else:
529
+ canopy_thresh_percentage = float(canopy_thresh_percentage)
530
+
531
+ if float(canopy_avoidance) <= 0.0:
532
+ canopy_avoidance = 0.0
533
+ if float(exponent) <= 0.0:
534
+ exponent = 1.0
535
+ # If OLnFID column is not found, column will be created
536
+ if 'OLnFID' not in line_seg.columns.array:
537
+ print("Created {} column in input line data.".format('OLnFID'))
538
+ line_seg['OLnFID'] = line_seg.index
539
+
540
+ if 'OLnSEG' not in line_seg.columns.array:
541
+ line_seg['OLnSEG'] = 0
542
+
543
+ print('%{}'.format(10))
544
+
545
+ # check coordinate systems between line and raster features
546
+ with rasterio.open(in_chm) as raster:
547
+ line_args = []
548
+
549
+ if compare_crs(vector_crs(in_line), raster_crs(in_chm)):
550
+ proc_segments = False
551
+ if proc_segments:
552
+ print("Splitting lines into segments...")
553
+ line_seg_split = split_into_segments(line_seg)
554
+ print("Splitting lines into segments...Done")
555
+ else:
556
+ if full_step:
557
+ print("Tool runs on input lines......")
558
+ line_seg_split = line_seg
559
+ else:
560
+ print("Tool runs on input segment lines......")
561
+ line_seg_split = split_into_Equal_Nth_segments(line_seg, 250)
562
+
563
+ print('%{}'.format(20))
564
+
565
+ work_in_bufferL1 = GeoDataFrame.copy(line_seg_split)
566
+ work_in_bufferL2 = GeoDataFrame.copy(line_seg_split)
567
+ work_in_bufferR1 = GeoDataFrame.copy(line_seg_split)
568
+ work_in_bufferR2 = GeoDataFrame.copy(line_seg_split)
569
+ work_in_bufferC = GeoDataFrame.copy(line_seg_split)
570
+ work_in_bufferL1['geometry'] = buffer(work_in_bufferL1['geometry'], distance=float(max_ln_width) + 1,
571
+ cap_style=3, single_sided=True)
572
+
573
+ work_in_bufferL2['geometry'] = buffer(work_in_bufferL2['geometry'], distance=-1,
574
+ cap_style=3, single_sided=True)
575
+
576
+ work_in_bufferL = GeoDataFrame(pd.concat([work_in_bufferL1, work_in_bufferL2]))
577
+ work_in_bufferL = work_in_bufferL.dissolve(by=['OLnFID', 'OLnSEG'], as_index=False)
578
+
579
+ work_in_bufferR1['geometry'] = buffer(work_in_bufferR1['geometry'], distance=-float(max_ln_width) - 1,
580
+ cap_style=3, single_sided=True)
581
+ work_in_bufferR2['geometry'] = buffer(work_in_bufferR2['geometry'], distance=1,
582
+ cap_style=3, single_sided=True)
583
+
584
+ work_in_bufferR = GeoDataFrame(pd.concat([work_in_bufferR1, work_in_bufferR2]))
585
+ work_in_bufferR = work_in_bufferR.dissolve(by=['OLnFID', 'OLnSEG'], as_index=False)
586
+
587
+ work_in_bufferC['geometry'] = buffer(work_in_bufferC['geometry'], distance=float(max_ln_width),
588
+ cap_style=3, single_sided=False)
589
+ print("Prepare arguments for Dynamic FP ...")
590
+ # line_argsL, line_argsR,line_argsC= generate_line_args(line_seg_split, work_in_bufferL,work_in_bufferC,
591
+ # raster, tree_radius, max_line_dist,
592
+ # canopy_avoidance, exponent, work_in_bufferR,
593
+ # canopy_thresh_percentage)
594
+
595
+ line_argsL, line_argsR, line_argsC = generate_line_args_DFP_NoClip(line_seg_split, work_in_bufferL,
596
+ work_in_bufferC, raster, in_chm,
597
+ tree_radius, max_line_dist,
598
+ canopy_avoidance, exponent,
599
+ work_in_bufferR,
600
+ canopy_thresh_percentage)
601
+
602
+ else:
603
+ print("Line and canopy raster spatial references are not same, please check.")
604
+ exit()
605
+ # pass center lines for footprint
606
+ print("Generating Dynamic footprint ...")
607
+
608
+ feat_listL = []
609
+ feat_listR = []
610
+ feat_listC = []
611
+ poly_listL = []
612
+ poly_listR = []
613
+ footprint_listL = []
614
+ footprint_listR = []
615
+ footprint_listC = []
616
+ # PARALLEL_MODE = ParallelMode.SEQUENTIAL
617
+ if PARALLEL_MODE == ParallelMode.MULTIPROCESSING:
618
+ # feat_listC = multiprocessing_footprint_relative(line_argsC, processes)
619
+ feat_listL = multiprocessing_footprint_relative(line_argsL, processes)
620
+ # feat_listL = execute_multiprocessing(process_single_line_relative,'Footprint',line_argsL, processes)
621
+ feat_listR = multiprocessing_footprint_relative(line_argsR, processes)
622
+ # feat_listR = execute_multiprocessing(process_single_line_relative, 'Footprint', line_argsR, processes)
623
+
624
+ elif PARALLEL_MODE == ParallelMode.SEQUENTIAL:
625
+ step = 1
626
+ total_steps = len(line_argsL)
627
+ print("There are {} result to process.".format(total_steps))
628
+ for row in line_argsL:
629
+ feat_listL.append(process_single_line_relative(row))
630
+ print("Footprint (left side) for line {} is done".format(step))
631
+ print(' "PROGRESS_LABEL Dynamic Line Footprint {} of {}" '.format(step, total_steps), flush=True)
632
+ print(' %{} '.format((step / total_steps) * 100))
633
+ step += 1
634
+ step = 1
635
+ total_steps = len(line_argsR)
636
+ for row in line_argsR:
637
+ feat_listR.append(process_single_line_relative(row))
638
+ print("Footprint for (right side) line {} is done".format(step))
639
+ print(' "PROGRESS_LABEL Dynamic Line Footprint {} of {}" '.format(step, total_steps), flush=True)
640
+ print(' %{} '.format((step / total_steps) * 100))
641
+ step += 1
642
+
643
+ print('%{}'.format(80))
644
+ print("Task done.")
645
+
646
+ for feat in feat_listL:
647
+ if feat:
648
+ footprint_listL.append(feat[0])
649
+ poly_listL.append(feat[1])
650
+
651
+ for feat in feat_listR:
652
+ if feat:
653
+ footprint_listR.append(feat[0])
654
+ poly_listR.append(feat[1])
655
+
656
+ print('Writing shapefile ...')
657
+ resultsL = GeoDataFrame(pd.concat(footprint_listL))
658
+ resultsL['geometry'] = resultsL['geometry'].buffer(0.005)
659
+ resultsR = GeoDataFrame(pd.concat(footprint_listR))
660
+ resultsR['geometry'] = resultsR['geometry'].buffer(0.005)
661
+ resultsL = resultsL.sort_values(by=['OLnFID', 'OLnSEG'])
662
+ resultsR = resultsR.sort_values(by=['OLnFID', 'OLnSEG'])
663
+ resultsL = resultsL.reset_index(drop=True)
664
+ resultsR = resultsR.reset_index(drop=True)
665
+ #
666
+
667
+ resultsAll = GeoDataFrame(pd.concat([resultsL, resultsR]))
668
+ dissolved_results = resultsAll.dissolve(by='OLnFID', as_index=False)
669
+ dissolved_results['geometry'] = dissolved_results['geometry'].buffer(-0.005)
670
+ print("Saving output ...")
671
+ dissolved_results.to_file(out_footprint)
672
+ print("Footprint file saved")
673
+
674
+ # dissolved polygon group by column 'OLnFID'
675
+ print("Generating centerlines from corridor polygons ...")
676
+ resultsCL = GeoDataFrame(pd.concat(poly_listL))
677
+ resultsCL['geometry'] = resultsCL['geometry'].buffer(0.005)
678
+ resultsCR = GeoDataFrame(pd.concat(poly_listR))
679
+ resultsCR['geometry'] = resultsCR['geometry'].buffer(0.005)
680
+
681
+ resultsCLR = GeoDataFrame(pd.concat([resultsCL, resultsCR]))
682
+ resultsCLR = resultsCLR.dissolve(by='OLnFID', as_index=False)
683
+ resultsCLR = resultsCLR.sort_values(by=['OLnFID', 'OLnSEG'])
684
+ resultsCLR = resultsCLR.reset_index(drop=True)
685
+ resultsCLR['geometry'] = resultsCLR['geometry'].buffer(-0.005)
686
+
687
+ # out_centerline=False
688
+ # save lines to file
689
+ if out_centerline:
690
+ poly_centerline_gpd = find_centerlines(resultsCLR, line_seg, processes)
691
+ poly_gpd = poly_centerline_gpd.copy()
692
+ centerline_gpd = poly_centerline_gpd.copy()
693
+
694
+ centerline_gpd = centerline_gpd.set_geometry('centerline')
695
+ centerline_gpd = centerline_gpd.drop(columns=['geometry'])
696
+ centerline_gpd.crs = poly_centerline_gpd.crs
697
+ centerline_gpd.to_file(out_centerline)
698
+ print("Centerline file saved")
699
+
700
+ # save polygons
701
+ path = Path(out_centerline)
702
+ path = path.with_stem(path.stem + '_poly')
703
+ poly_gpd = poly_gpd.drop(columns=['centerline'])
704
+ poly_gpd.to_file(path)
705
+
706
+ print('%{}'.format(100))
707
+
708
+
709
+ if __name__ == '__main__':
710
+ start_time = time.time()
711
+ print('Dynamic Footprint processing started')
712
+ print('Current time: {}'.format(time.strftime("%d %b %Y %H:%M:%S", time.localtime())))
713
+
714
+ in_args, in_verbose = check_arguments()
715
+ main_line_footprint_relative(print, **in_args.input, processes=int(in_args.processes), verbose=in_verbose)
716
+
717
+ print('%{}'.format(100))
718
+ print('Dynamic Footprint processing finished')
719
+ print('Current time: {}'.format(time.strftime("%d %b %Y %H:%M:%S", time.localtime())))
720
+ print('Total processing time (seconds): {}'.format(round(time.time() - start_time, 3)))