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