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,351 +1,491 @@
1
- import numpy as np
2
- from rasterio import features
3
- import shapely
4
- from shapely.geometry import shape
5
- from shapely.ops import unary_union, substring, linemerge, nearest_points, split
6
- from shapely.geometry import Point, MultiPoint, Polygon, MultiPolygon, LineString, MultiLineString
7
- # from beratools.third_party.label_centerlines import get_centerline
8
- from label_centerlines import get_centerline
9
-
10
- from beratools.core.tool_base import *
11
- from beratools.core.constants import *
12
- from beratools.tools.common import generate_perpendicular_line_precise
13
-
14
-
15
- def centerline_is_valid(centerline, input_line):
16
- """
17
- Check if centerline is valid
18
- Parameters
19
- ----------
20
- centerline :
21
- input_line : shapely LineString
22
- This can be input seed line or least cost path. Only two end points are used.
23
-
24
- Returns
25
- -------
26
-
27
- """
28
- if not centerline:
29
- return False
30
-
31
- # centerline length less the half of least cost path
32
- if (centerline.length < input_line.length / 2 or
33
- centerline.distance(Point(input_line.coords[0])) > BT_EPSILON or
34
- centerline.distance(Point(input_line.coords[-1])) > BT_EPSILON):
35
- return False
36
-
37
- return True
38
-
39
-
40
- def snap_end_to_end(in_line, line_reference):
41
- if type(in_line) is MultiLineString:
42
- in_line = linemerge(in_line)
43
- if type(in_line) is MultiLineString:
44
- print(f'algo_centerline: MultiLineString found {in_line.centroid}, pass.')
45
- return None
46
-
47
- pts = list(in_line.coords)
48
- if len(pts) < 2:
49
- print('snap_end_to_end: input line invalid.')
50
- return in_line
51
-
52
- line_start = Point(pts[0])
53
- line_end = Point(pts[-1])
54
- ref_ends = MultiPoint([line_reference.coords[0], line_reference.coords[-1]])
55
-
56
- _, snap_start = nearest_points(line_start, ref_ends)
57
- _, snap_end = nearest_points(line_end, ref_ends)
58
-
59
- if in_line.has_z:
60
- snap_start = shapely.force_3d(snap_start)
61
- snap_end = shapely.force_3d(snap_end)
62
- else:
63
- snap_start = shapely.force_2d(snap_start)
64
- snap_end = shapely.force_2d(snap_end)
65
-
66
- pts[0] = snap_start.coords[0]
67
- pts[-1] = snap_end.coords[0]
68
-
69
- return LineString(pts)
70
-
71
-
72
- def find_centerline(poly, input_line):
73
- """
74
- Parameters
75
- ----------
76
- poly : Polygon
77
- input_line : LineString
78
- Least cost path or seed line
79
-
80
- Returns
81
- -------
82
-
83
- """
84
- default_return = input_line, CenterlineStatus.FAILED
85
- if not poly:
86
- print('find_centerline: No polygon found')
87
- return default_return
88
-
89
- poly = shapely.segmentize(poly, max_segment_length=CL_SEGMENTIZE_LENGTH)
90
-
91
- poly = poly.buffer(CL_POLYGON_BUFFER) # buffer polygon to reduce MultiPolygons
92
- if type(poly) is MultiPolygon:
93
- print('MultiPolygon encountered, skip.')
94
- return default_return
95
-
96
- exterior_pts = list(poly.exterior.coords)
97
-
98
- if CL_DELETE_HOLES:
99
- poly = Polygon(exterior_pts)
100
- if CL_SIMPLIFY_POLYGON:
101
- poly = poly.simplify(CL_SIMPLIFY_LENGTH)
102
-
103
- line_coords = list(input_line.coords)
104
-
105
- # TODO add more code to filter voronoi vertices
106
- src_geom = Point(line_coords[0]).buffer(CL_BUFFER_CLIP*3).intersection(poly)
107
- dst_geom = Point(line_coords[-1]).buffer(CL_BUFFER_CLIP*3).intersection(poly)
108
- src_geom = None
109
- dst_geom = None
110
-
111
- try:
112
- centerline = get_centerline(poly, segmentize_maxlen=1, max_points=3000,
113
- simplification=0.05, smooth_sigma=CL_SMOOTH_SIGMA, max_paths=1,
114
- src_geom=src_geom, dst_geom=dst_geom)
115
- except Exception as e:
116
- print(e)
117
- return default_return
118
-
119
- if not centerline:
120
- return default_return
121
-
122
- if type(centerline) is MultiLineString:
123
- if len(centerline.geoms) > 1:
124
- print(" Multiple centerline segments detected, no further processing.")
125
- return centerline, CenterlineStatus.SUCCESS # TODO: inspect
126
- elif len(centerline.geoms) == 1:
127
- centerline = centerline.geoms[0]
128
- else:
129
- return default_return
130
-
131
- cl_coords = list(centerline.coords)
132
-
133
- # trim centerline at two ends
134
- head_buffer = Point(cl_coords[0]).buffer(CL_BUFFER_CLIP)
135
- centerline = centerline.difference(head_buffer)
136
-
137
- end_buffer = Point(cl_coords[-1]).buffer(CL_BUFFER_CLIP)
138
- centerline = centerline.difference(end_buffer)
139
-
140
- if not centerline:
141
- print('No centerline detected, use input line instead.')
142
- return default_return
143
- try:
144
- if centerline.is_empty:
145
- print('Empty centerline detected, use input line instead.')
146
- return default_return
147
- except Exception as e:
148
- print(e)
149
-
150
- centerline = snap_end_to_end(centerline, input_line)
151
-
152
- # Check if centerline is valid. If not, regenerate by splitting polygon into two halves.
153
- if not centerline_is_valid(centerline, input_line):
154
- try:
155
- print(f'Regenerating line ...')
156
- centerline = regenerate_centerline(poly, input_line)
157
- return centerline, CenterlineStatus.REGENERATE_SUCCESS
158
- except Exception as e:
159
- print('find_centerline: Exception occurred. \n {}'.format(e))
160
- return input_line, CenterlineStatus.REGENERATE_FAILED
161
-
162
- return centerline, CenterlineStatus.SUCCESS
163
-
164
-
165
- # def find_route(array, start, end, fully_connected, geometric):
166
- # route_list, cost_list = route_through_array(array, start, end, fully_connected, geometric)
167
- # return route_list, cost_list
168
-
169
-
170
- def find_corridor_polygon(corridor_thresh, in_transform, line_gpd):
171
- # Threshold corridor raster used for generating centerline
172
- corridor_thresh_cl = np.ma.where(corridor_thresh == 0.0, 1, 0).data
173
- corridor_mask = np.where(1 == corridor_thresh_cl, True, False)
174
- poly_generator = features.shapes(corridor_thresh_cl, mask=corridor_mask, transform=in_transform)
175
- corridor_polygon = []
176
-
177
- try:
178
- for poly, value in poly_generator:
179
- if shape(poly).area > 1:
180
- corridor_polygon.append(shape(poly))
181
- except Exception as e:
182
- print(e)
183
-
184
- if corridor_polygon:
185
- corridor_polygon = (unary_union(corridor_polygon))
186
- if type(corridor_polygon) is MultiPolygon:
187
- poly_list = shapely.get_parts(corridor_polygon)
188
- merge_poly = poly_list[0]
189
- for i in range(1, len(poly_list)):
190
- if shapely.intersects(merge_poly, poly_list[i]):
191
- merge_poly = shapely.union(merge_poly, poly_list[i])
192
- else:
193
- buffer_dist = poly_list[i].distance(merge_poly) + 0.1
194
- buffer_poly = poly_list[i].buffer(buffer_dist)
195
- merge_poly = shapely.union(merge_poly, buffer_poly)
196
- corridor_polygon = merge_poly
197
- else:
198
- corridor_polygon = None
199
-
200
- # create GeoDataFrame for centerline
201
- corridor_poly_gpd = gpd.GeoDataFrame.copy(line_gpd)
202
- corridor_poly_gpd.geometry = [corridor_polygon]
203
-
204
- return corridor_poly_gpd
205
-
206
-
207
- def process_single_centerline(row_and_path):
208
- """
209
-
210
- Parameters
211
- ----------
212
- row_and_path:
213
- list of row (polygon and props) and least cost path
214
- first is geopandas row, second is input line, (least cost path)
215
-
216
- Returns
217
- -------
218
-
219
- """
220
- row = row_and_path[0]
221
- lc_path = row_and_path[1]
222
-
223
- poly = row.geometry.iloc[0]
224
- centerline, status = find_centerline(poly, lc_path)
225
- row['centerline'] = centerline
226
-
227
- return row
228
-
229
-
230
- def find_centerlines(poly_gpd, line_seg, processes):
231
- centerline = None
232
- centerline_gpd = []
233
- rows_and_paths = []
234
-
235
- try:
236
- for i in poly_gpd.index:
237
- row = poly_gpd.loc[[i]]
238
- poly = row.geometry.iloc[0]
239
- if 'OLnSEG' in line_seg.columns:
240
- line_id, Seg_id = row['OLnFID'].iloc[0], row['OLnSEG'].iloc[0]
241
- lc_path = line_seg.loc[(line_seg.OLnFID == line_id) & (line_seg.OLnSEG == Seg_id)]['geometry'].iloc[0]
242
- else:
243
- line_id = row['OLnFID'].iloc[0]
244
- lc_path = line_seg.loc[(line_seg.OLnFID == line_id)]['geometry'].iloc[0]
245
-
246
- rows_and_paths.append((row, lc_path))
247
- except Exception as e:
248
- print(e)
249
-
250
- total_steps = len(rows_and_paths)
251
- step = 0
252
-
253
- # if PARALLEL_MODE == ParallelMode.MULTIPROCESSING:
254
- # with Pool(processes=processes) as pool:
255
- # # execute tasks in order, process results out of order
256
- # for result in pool.imap_unordered(process_single_centerline, rows_and_paths):
257
- # centerline_gpd.append(result)
258
- # step += 1
259
- # print(' "PROGRESS_LABEL Centerline {} of {}" '.format(step, total_steps), flush=True)
260
- # print(' %{} '.format(step / total_steps * 100))
261
- # print('Centerline No. {} done'.format(step))
262
- # elif PARALLEL_MODE == ParallelMode.SEQUENTIAL:
263
- # for item in rows_and_paths:
264
- # row_with_centerline = process_single_centerline(item)
265
- # centerline_gpd.append(row_with_centerline)
266
- # step += 1
267
- # print(' "PROGRESS_LABEL Centerline {} of {}" '.format(step, total_steps), flush=True)
268
- # print(' %{} '.format(step / total_steps * 100))
269
- # print('Centerline No. {} done'.format(step))
270
- centerline_gpd = execute_multiprocessing(process_single_centerline, rows_and_paths,
271
- 'find_centerlines', processes, 1)
272
- return pd.concat(centerline_gpd)
273
-
274
-
275
- def regenerate_centerline(poly, input_line):
276
- """
277
- Regenerates centerline when initial
278
- ----------
279
- poly : line is not valid
280
- Parameters
281
- input_line : shapely LineString
282
- This can be input seed line or least cost path. Only two end points will be used
283
-
284
- Returns
285
- -------
286
-
287
- """
288
- line_1 = substring(input_line, start_dist=0.0, end_dist=input_line.length / 2)
289
- line_2 = substring(input_line, start_dist=input_line.length / 2, end_dist=input_line.length)
290
-
291
- pts = shapely.force_2d([Point(list(input_line.coords)[0]),
292
- Point(list(line_1.coords)[-1]),
293
- Point(list(input_line.coords)[-1])])
294
- perp = generate_perpendicular_line_precise(pts)
295
-
296
- # MultiPolygon is rare, but need to be dealt with
297
- # remove polygon of area less than CL_CLEANUP_POLYGON_BY_AREA
298
- poly = poly.buffer(CL_POLYGON_BUFFER)
299
- if type(poly) is MultiPolygon:
300
- poly_geoms = list(poly.geoms)
301
- poly_valid = [True] * len(poly_geoms)
302
- for i, item in enumerate(poly_geoms):
303
- if item.area < CL_CLEANUP_POLYGON_BY_AREA:
304
- poly_valid[i] = False
305
-
306
- poly_geoms = list(compress(poly_geoms, poly_valid))
307
- if len(poly_geoms) != 1: # still multi polygon
308
- print('regenerate_centerline: Multi or none polygon found, pass.')
309
-
310
- poly = Polygon(poly_geoms[0])
311
-
312
- poly_exterior = Polygon(poly.buffer(CL_POLYGON_BUFFER).exterior)
313
- poly_split = split(poly_exterior, perp)
314
-
315
- if len(poly_split.geoms) < 2:
316
- print('regenerate_centerline: polygon split failed, pass.')
317
- return None
318
-
319
- poly_1 = poly_split.geoms[0]
320
- poly_2 = poly_split.geoms[1]
321
-
322
- # find polygon and line pairs
323
- pair_line_1 = line_1
324
- pair_line_2 = line_2
325
- if not poly_1.intersects(line_1):
326
- pair_line_1 = line_2
327
- pair_line_2 = line_1
328
- elif poly_1.intersection(line_1).length < line_1.length / 3:
329
- pair_line_1 = line_2
330
- pair_line_2 = line_1
331
-
332
- center_line_1 = find_centerline(poly_1, pair_line_1)
333
- center_line_2 = find_centerline(poly_2, pair_line_2)
334
-
335
- center_line_1 = center_line_1[0]
336
- center_line_2 = center_line_2[0]
337
-
338
- if not center_line_1 or not center_line_2:
339
- print('Regenerate line: centerline is None')
340
- return None
341
-
342
- try:
343
- if center_line_1.is_empty or center_line_2.is_empty:
344
- print('Regenerate line: centerline is empty')
345
- return None
346
- except Exception as e:
347
- print(e)
348
-
349
- print(f'Centerline is regenerated.')
350
- return linemerge(MultiLineString([center_line_1, center_line_2]))
351
-
1
+ """
2
+ Copyright (C) 2025 Applied Geospatial Research Group.
3
+
4
+ This script is licensed under the GNU General Public License v3.0.
5
+ See <https://gnu.org/licenses/gpl-3.0> for full license details.
6
+
7
+ Author: Richard Zeng
8
+
9
+ Description:
10
+ This script is part of the BERA Tools.
11
+ Webpage: https://github.com/appliedgrg/beratools
12
+
13
+ This file is intended to be hosting algorithms and utility functions/classes
14
+ for centerline tool.
15
+ """
16
+ import enum
17
+ from itertools import compress
18
+
19
+ import geopandas as gpd
20
+ import numpy as np
21
+ import pandas as pd
22
+ import rasterio
23
+ import shapely
24
+ import shapely.geometry as sh_geom
25
+ import shapely.ops as sh_ops
26
+ from label_centerlines import get_centerline
27
+
28
+ import beratools.core.algo_common as algo_common
29
+ import beratools.core.algo_cost as algo_cost
30
+ import beratools.core.algo_dijkstra as bt_dijkstra
31
+ import beratools.core.constants as bt_const
32
+ import beratools.core.tool_base as bt_base
33
+ import beratools.tools.common as bt_common
34
+
35
+
36
+ class CenterlineParams(float, enum.Enum):
37
+ BUFFER_CLIP = 5.0
38
+ SEGMENTIZE_LENGTH = 1.0
39
+ SIMPLIFY_LENGTH = 0.5
40
+ SMOOTH_SIGMA = 0.8
41
+ CLEANUP_POLYGON_BY_AREA = 1.0
42
+
43
+ @enum.unique
44
+ class CenterlineStatus(enum.IntEnum):
45
+ SUCCESS = 1
46
+ FAILED = 2
47
+ REGENERATE_SUCCESS = 3
48
+ REGENERATE_FAILED = 4
49
+
50
+ def centerline_is_valid(centerline, input_line):
51
+ """
52
+ Check if centerline is valid.
53
+
54
+ Args:
55
+ centerline (_type_): _description_
56
+ input_line (sh_geom.LineString): Seed line or least cost path.
57
+ Only two end points are used.
58
+
59
+ Returns:
60
+ bool: True if line is valid
61
+
62
+ """
63
+ if not centerline:
64
+ return False
65
+
66
+ # centerline length less the half of least cost path
67
+ if (
68
+ centerline.length < input_line.length / 2
69
+ or centerline.distance(sh_geom.Point(input_line.coords[0]))
70
+ > bt_const.BT_EPSILON
71
+ or centerline.distance(sh_geom.Point(input_line.coords[-1]))
72
+ > bt_const.BT_EPSILON
73
+ ):
74
+ return False
75
+
76
+ return True
77
+
78
+
79
+ def snap_end_to_end(in_line, line_reference):
80
+ if type(in_line) is sh_geom.MultiLineString:
81
+ in_line = sh_ops.linemerge(in_line)
82
+ if type(in_line) is sh_geom.MultiLineString:
83
+ print(f'algo_centerline: MultiLineString found {in_line.centroid}, pass.')
84
+ return None
85
+
86
+ pts = list(in_line.coords)
87
+ if len(pts) < 2:
88
+ print('snap_end_to_end: input line invalid.')
89
+ return in_line
90
+
91
+ line_start = sh_geom.Point(pts[0])
92
+ line_end = sh_geom.Point(pts[-1])
93
+ ref_ends = sh_geom.MultiPoint([line_reference.coords[0], line_reference.coords[-1]])
94
+
95
+ _, snap_start = sh_ops.nearest_points(line_start, ref_ends)
96
+ _, snap_end = sh_ops.nearest_points(line_end, ref_ends)
97
+
98
+ if in_line.has_z:
99
+ snap_start = shapely.force_3d(snap_start)
100
+ snap_end = shapely.force_3d(snap_end)
101
+ else:
102
+ snap_start = shapely.force_2d(snap_start)
103
+ snap_end = shapely.force_2d(snap_end)
104
+
105
+ pts[0] = snap_start.coords[0]
106
+ pts[-1] = snap_end.coords[0]
107
+
108
+ return sh_geom.LineString(pts)
109
+
110
+
111
+ def find_centerline(poly, input_line):
112
+ """
113
+ Find centerline from polygon and input line.
114
+
115
+ Args:
116
+ poly : sh_geom.Polygon
117
+ input_line ( sh_geom.LineString): Least cost path or seed line
118
+
119
+ Returns:
120
+ centerline (sh_geom.LineString): Centerline
121
+ status (CenterlineStatus): Status of centerline generation
122
+
123
+ """
124
+ default_return = input_line, CenterlineStatus.FAILED
125
+ if not poly:
126
+ print('find_centerline: No polygon found')
127
+ return default_return
128
+
129
+ poly = shapely.segmentize(
130
+ poly, max_segment_length=CenterlineParams.SEGMENTIZE_LENGTH
131
+ )
132
+
133
+ # buffer to reduce MultiPolygons
134
+ poly = poly.buffer(bt_const.SMALL_BUFFER)
135
+ if type(poly) is sh_geom.MultiPolygon:
136
+ print('sh_geom.MultiPolygon encountered, skip.')
137
+ return default_return
138
+
139
+ exterior_pts = list(poly.exterior.coords)
140
+
141
+ if bt_const.CenterlineFlags.DELETE_HOLES:
142
+ poly = sh_geom.Polygon(exterior_pts)
143
+ if bt_const.CenterlineFlags.SIMPLIFY_POLYGON:
144
+ poly = poly.simplify(CenterlineParams.SIMPLIFY_LENGTH)
145
+
146
+ line_coords = list(input_line.coords)
147
+
148
+ # TODO add more code to filter Voronoi vertices
149
+ src_geom = (
150
+ sh_geom.Point(line_coords[0])
151
+ .buffer(CenterlineParams.BUFFER_CLIP * 3)
152
+ .intersection(poly)
153
+ )
154
+ dst_geom = (
155
+ sh_geom.Point(line_coords[-1])
156
+ .buffer(CenterlineParams.BUFFER_CLIP * 3)
157
+ .intersection(poly)
158
+ )
159
+ src_geom = None
160
+ dst_geom = None
161
+
162
+ try:
163
+ centerline = get_centerline(
164
+ poly,
165
+ segmentize_maxlen=1,
166
+ max_points=3000,
167
+ simplification=0.05,
168
+ smooth_sigma=CenterlineParams.SMOOTH_SIGMA,
169
+ max_paths=1,
170
+ src_geom=src_geom,
171
+ dst_geom=dst_geom,
172
+ )
173
+ except Exception as e:
174
+ print(f'find_centerline: {e}')
175
+ return default_return
176
+
177
+ if not centerline:
178
+ return default_return
179
+
180
+ if type(centerline) is sh_geom.MultiLineString:
181
+ if len(centerline.geoms) > 1:
182
+ print(" Multiple centerline segments detected, no further processing.")
183
+ return centerline, CenterlineStatus.SUCCESS # TODO: inspect
184
+ elif len(centerline.geoms) == 1:
185
+ centerline = centerline.geoms[0]
186
+ else:
187
+ return default_return
188
+
189
+ cl_coords = list(centerline.coords)
190
+
191
+ # trim centerline at two ends
192
+ head_buffer = sh_geom.Point(cl_coords[0]).buffer(
193
+ CenterlineParams.BUFFER_CLIP
194
+ )
195
+ centerline = centerline.difference(head_buffer)
196
+
197
+ end_buffer = sh_geom.Point(cl_coords[-1]).buffer(
198
+ CenterlineParams.BUFFER_CLIP
199
+ )
200
+ centerline = centerline.difference(end_buffer)
201
+
202
+ # No centerline detected, use input line instead.
203
+ if not centerline:
204
+ return default_return
205
+ try:
206
+ # Empty centerline detected, use input line instead.
207
+ if centerline.is_empty:
208
+ return default_return
209
+ except Exception as e:
210
+ print(f'find_centerline: {e}')
211
+
212
+ centerline = snap_end_to_end(centerline, input_line)
213
+
214
+ # Check centerline. If valid, regenerate by splitting polygon into two halves.
215
+ if not centerline_is_valid(centerline, input_line):
216
+ try:
217
+ print('Regenerating line ...')
218
+ centerline = regenerate_centerline(poly, input_line)
219
+ return centerline, CenterlineStatus.REGENERATE_SUCCESS
220
+ except Exception as e:
221
+ print(f'find_centerline: {e}')
222
+ return input_line, CenterlineStatus.REGENERATE_FAILED
223
+
224
+ return centerline, CenterlineStatus.SUCCESS
225
+
226
+
227
+ def find_corridor_polygon(corridor_thresh, in_transform, line_gpd):
228
+ # Threshold corridor raster used for generating centerline
229
+ corridor_thresh_cl = np.ma.where(corridor_thresh == 0.0, 1, 0).data
230
+ if corridor_thresh_cl.dtype == np.int64:
231
+ corridor_thresh_cl = corridor_thresh_cl.astype(np.int32)
232
+
233
+ corridor_mask = np.where(1 == corridor_thresh_cl, True, False)
234
+ poly_generator = rasterio.features.shapes(
235
+ corridor_thresh_cl, mask=corridor_mask, transform=in_transform
236
+ )
237
+ corridor_polygon = []
238
+
239
+ try:
240
+ for poly, value in poly_generator:
241
+ if sh_geom.shape(poly).area > 1:
242
+ corridor_polygon.append(sh_geom.shape(poly))
243
+ except Exception as e:
244
+ print(f"find_corridor_polygon: {e}")
245
+
246
+ if corridor_polygon:
247
+ corridor_polygon = (sh_ops.unary_union(corridor_polygon))
248
+ if type(corridor_polygon) is sh_geom.MultiPolygon:
249
+ poly_list = shapely.get_parts(corridor_polygon)
250
+ merge_poly = poly_list[0]
251
+ for i in range(1, len(poly_list)):
252
+ if shapely.intersects(merge_poly, poly_list[i]):
253
+ merge_poly = shapely.union(merge_poly, poly_list[i])
254
+ else:
255
+ buffer_dist = poly_list[i].distance(merge_poly) + 0.1
256
+ buffer_poly = poly_list[i].buffer(buffer_dist)
257
+ merge_poly = shapely.union(merge_poly, buffer_poly)
258
+ corridor_polygon = merge_poly
259
+ else:
260
+ corridor_polygon = None
261
+
262
+ # create GeoDataFrame for centerline
263
+ corridor_poly_gpd = gpd.GeoDataFrame.copy(line_gpd)
264
+ corridor_poly_gpd.geometry = [corridor_polygon]
265
+
266
+ return corridor_poly_gpd
267
+
268
+
269
+ def process_single_centerline(row_and_path):
270
+ """
271
+ Find centerline.
272
+
273
+ Args:
274
+ row_and_path (list of row (gdf and lc_path)): and least cost path
275
+ first is GeoPandas row, second is input line, (least cost path)
276
+
277
+ Returns:
278
+ row: GeoPandas row with centerline
279
+
280
+ """
281
+ row = row_and_path[0]
282
+ lc_path = row_and_path[1]
283
+
284
+ poly = row.geometry.iloc[0]
285
+ centerline, status = find_centerline(poly, lc_path)
286
+ row['centerline'] = centerline
287
+
288
+ return row
289
+
290
+
291
+ def find_centerlines(poly_gpd, line_seg, processes):
292
+ centerline_gpd = []
293
+ rows_and_paths = []
294
+
295
+ try:
296
+ for i in poly_gpd.index:
297
+ row = poly_gpd.loc[[i]]
298
+ if 'OLnSEG' in line_seg.columns:
299
+ line_id, Seg_id = row['OLnFID'].iloc[0], row['OLnSEG'].iloc[0]
300
+ lc_path = line_seg.loc[
301
+ (line_seg.OLnFID == line_id) & (line_seg.OLnSEG == Seg_id)
302
+ ]["geometry"].iloc[0]
303
+ else:
304
+ line_id = row['OLnFID'].iloc[0]
305
+ lc_path = line_seg.loc[(line_seg.OLnFID == line_id)]['geometry'].iloc[0]
306
+
307
+ rows_and_paths.append((row, lc_path))
308
+ except Exception as e:
309
+ print(f"find_centerlines: {e}")
310
+
311
+ centerline_gpd = bt_base.execute_multiprocessing(
312
+ process_single_centerline, rows_and_paths, "find_centerlines", processes, 1
313
+ )
314
+ return pd.concat(centerline_gpd)
315
+
316
+
317
+ def regenerate_centerline(poly, input_line):
318
+ """
319
+ Regenerates centerline when initial poly is not valid.
320
+
321
+ Args:
322
+ input_line (sh_geom.LineString): Seed line or least cost path.
323
+ Only two end points will be used
324
+
325
+ Returns:
326
+ sh_geom.MultiLineString
327
+
328
+ """
329
+ line_1 = sh_ops.substring(
330
+ input_line, start_dist=0.0, end_dist=input_line.length / 2
331
+ )
332
+ line_2 = sh_ops.substring(
333
+ input_line, start_dist=input_line.length / 2, end_dist=input_line.length
334
+ )
335
+
336
+ pts = shapely.force_2d(
337
+ [
338
+ sh_geom.Point(list(input_line.coords)[0]),
339
+ sh_geom.Point(list(line_1.coords)[-1]),
340
+ sh_geom.Point(list(input_line.coords)[-1]),
341
+ ]
342
+ )
343
+ perp = algo_common.generate_perpendicular_line_precise(pts)
344
+
345
+ # sh_geom.MultiPolygon is rare, but need to be dealt with
346
+ # remove polygon of area less than CenterlineParams.CLEANUP_POLYGON_BY_AREA
347
+ poly = poly.buffer(bt_const.SMALL_BUFFER)
348
+ if type(poly) is sh_geom.MultiPolygon:
349
+ poly_geoms = list(poly.geoms)
350
+ poly_valid = [True] * len(poly_geoms)
351
+ for i, item in enumerate(poly_geoms):
352
+ if item.area < CenterlineParams.CLEANUP_POLYGON_BY_AREA:
353
+ poly_valid[i] = False
354
+
355
+ poly_geoms = list(compress(poly_geoms, poly_valid))
356
+ if len(poly_geoms) != 1: # still multi polygon
357
+ print("regenerate_centerline: Multi or none polygon found, pass.")
358
+
359
+ poly = sh_geom.Polygon(poly_geoms[0])
360
+
361
+ poly_exterior = sh_geom.Polygon(
362
+ poly.buffer(bt_const.SMALL_BUFFER).exterior
363
+ )
364
+ poly_split = sh_ops.split(poly_exterior, perp)
365
+
366
+ if len(poly_split.geoms) < 2:
367
+ print("regenerate_centerline: polygon sh_ops.split failed, pass.")
368
+ return None
369
+
370
+ poly_1 = poly_split.geoms[0]
371
+ poly_2 = poly_split.geoms[1]
372
+
373
+ # find polygon and line pairs
374
+ pair_line_1 = line_1
375
+ pair_line_2 = line_2
376
+ if not poly_1.intersects(line_1):
377
+ pair_line_1 = line_2
378
+ pair_line_2 = line_1
379
+ elif poly_1.intersection(line_1).length < line_1.length / 3:
380
+ pair_line_1 = line_2
381
+ pair_line_2 = line_1
382
+
383
+ center_line_1 = find_centerline(poly_1, pair_line_1)
384
+ center_line_2 = find_centerline(poly_2, pair_line_2)
385
+
386
+ center_line_1 = center_line_1[0]
387
+ center_line_2 = center_line_2[0]
388
+
389
+ if not center_line_1 or not center_line_2:
390
+ print("Regenerate line: centerline is None")
391
+ return None
392
+
393
+ try:
394
+ if center_line_1.is_empty or center_line_2.is_empty:
395
+ print("Regenerate line: centerline is empty")
396
+ return None
397
+ except Exception as e:
398
+ print(f"regenerate_centerline: {e}")
399
+
400
+ print("Centerline is regenerated.")
401
+ return sh_ops.linemerge(sh_geom.MultiLineString([center_line_1, center_line_2]))
402
+
403
+ class SeedLine:
404
+ """Class to store seed line and least cost path."""
405
+
406
+ def __init__(self, line_gdf, ras_file, proc_segments, line_radius):
407
+ self.line = line_gdf
408
+ self.raster = ras_file
409
+ self.line_radius = line_radius
410
+ self.lc_path = None
411
+ self.centerline = None
412
+ self.corridor_poly_gpd = None
413
+
414
+ def compute(self):
415
+ line = self.line.geometry[0]
416
+ line_radius = self.line_radius
417
+ in_raster = self.raster
418
+ seed_line = line # LineString
419
+ default_return = (seed_line, seed_line, None)
420
+
421
+ ras_clip, out_meta = bt_common.clip_raster(in_raster, seed_line, line_radius)
422
+ cost_clip, _ = algo_cost.cost_raster(ras_clip, out_meta)
423
+
424
+ lc_path = line
425
+ try:
426
+ if bt_const.CenterlineFlags.USE_SKIMAGE_GRAPH:
427
+ lc_path = bt_dijkstra.find_least_cost_path_skimage(
428
+ cost_clip, out_meta, seed_line
429
+ )
430
+ else:
431
+ lc_path = bt_dijkstra.find_least_cost_path(
432
+ cost_clip, out_meta, seed_line
433
+ )
434
+ except Exception as e:
435
+ print(e)
436
+ return default_return
437
+
438
+ if lc_path:
439
+ lc_path_coords = lc_path.coords
440
+ else:
441
+ lc_path_coords = []
442
+
443
+ self.lc_path = lc_path
444
+
445
+ # search for centerline
446
+ if len(lc_path_coords) < 2:
447
+ print("No least cost path detected, use input line.")
448
+ self.line["status"] = CenterlineStatus.FAILED.value
449
+ return default_return
450
+
451
+ # get corridor raster
452
+ lc_path = sh_geom.LineString(lc_path_coords)
453
+ ras_clip, out_meta = bt_common.clip_raster(
454
+ in_raster, lc_path, line_radius * 0.9
455
+ )
456
+ cost_clip, _ = algo_cost.cost_raster(ras_clip, out_meta)
457
+
458
+ out_transform = out_meta["transform"]
459
+ transformer = rasterio.transform.AffineTransformer(out_transform)
460
+ cell_size = (out_transform[0], -out_transform[4])
461
+
462
+ x1, y1 = lc_path_coords[0]
463
+ x2, y2 = lc_path_coords[-1]
464
+ source = [transformer.rowcol(x1, y1)]
465
+ destination = [transformer.rowcol(x2, y2)]
466
+ corridor_thresh_cl = algo_common.corridor_raster(
467
+ cost_clip,
468
+ out_meta,
469
+ source,
470
+ destination,
471
+ cell_size,
472
+ bt_const.FP_CORRIDOR_THRESHOLD,
473
+ )
474
+
475
+ # find contiguous corridor polygon and extract centerline
476
+ df = gpd.GeoDataFrame(geometry=[seed_line], crs=out_meta["crs"])
477
+ corridor_poly_gpd = find_corridor_polygon(
478
+ corridor_thresh_cl, out_transform, df
479
+ )
480
+ center_line, status = find_centerline(
481
+ corridor_poly_gpd.geometry.iloc[0], lc_path
482
+ )
483
+ self.line["status"] = status.value
484
+
485
+ self.lc_path = self.line.copy()
486
+ self.lc_path.geometry = [lc_path]
487
+
488
+ self.centerline = self.line.copy()
489
+ self.centerline.geometry = [center_line]
490
+
491
+ self.corridor_poly_gpd = corridor_poly_gpd