BERATools 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- beratools/__init__.py +3 -0
- beratools/core/__init__.py +0 -0
- beratools/core/algo_centerline.py +476 -0
- beratools/core/algo_common.py +489 -0
- beratools/core/algo_cost.py +185 -0
- beratools/core/algo_dijkstra.py +492 -0
- beratools/core/algo_footprint_rel.py +693 -0
- beratools/core/algo_line_grouping.py +941 -0
- beratools/core/algo_merge_lines.py +255 -0
- beratools/core/algo_split_with_lines.py +296 -0
- beratools/core/algo_vertex_optimization.py +451 -0
- beratools/core/constants.py +56 -0
- beratools/core/logger.py +92 -0
- beratools/core/tool_base.py +126 -0
- beratools/gui/__init__.py +11 -0
- beratools/gui/assets/BERALogo.png +0 -0
- beratools/gui/assets/beratools.json +471 -0
- beratools/gui/assets/closed.gif +0 -0
- beratools/gui/assets/closed.png +0 -0
- beratools/gui/assets/gui.json +8 -0
- beratools/gui/assets/open.gif +0 -0
- beratools/gui/assets/open.png +0 -0
- beratools/gui/assets/tool.gif +0 -0
- beratools/gui/assets/tool.png +0 -0
- beratools/gui/bt_data.py +485 -0
- beratools/gui/bt_gui_main.py +700 -0
- beratools/gui/main.py +27 -0
- beratools/gui/tool_widgets.py +730 -0
- beratools/tools/__init__.py +7 -0
- beratools/tools/canopy_threshold_relative.py +769 -0
- beratools/tools/centerline.py +127 -0
- beratools/tools/check_seed_line.py +48 -0
- beratools/tools/common.py +622 -0
- beratools/tools/line_footprint_absolute.py +203 -0
- beratools/tools/line_footprint_fixed.py +480 -0
- beratools/tools/line_footprint_functions.py +884 -0
- beratools/tools/line_footprint_relative.py +75 -0
- beratools/tools/tool_template.py +72 -0
- beratools/tools/vertex_optimization.py +57 -0
- beratools-0.1.0.dist-info/METADATA +134 -0
- beratools-0.1.0.dist-info/RECORD +44 -0
- beratools-0.1.0.dist-info/WHEEL +4 -0
- beratools-0.1.0.dist-info/entry_points.txt +2 -0
- beratools-0.1.0.dist-info/licenses/LICENSE +22 -0
beratools/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,476 @@
|
|
|
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
|
+
|
|
17
|
+
import enum
|
|
18
|
+
from itertools import compress
|
|
19
|
+
|
|
20
|
+
import geopandas as gpd
|
|
21
|
+
import numpy as np
|
|
22
|
+
import pandas as pd
|
|
23
|
+
import rasterio
|
|
24
|
+
import shapely
|
|
25
|
+
import shapely.geometry as sh_geom
|
|
26
|
+
import shapely.ops as sh_ops
|
|
27
|
+
from label_centerlines import get_centerline
|
|
28
|
+
|
|
29
|
+
import beratools.core.algo_common as algo_common
|
|
30
|
+
import beratools.core.algo_cost as algo_cost
|
|
31
|
+
import beratools.core.algo_dijkstra as bt_dijkstra
|
|
32
|
+
import beratools.core.constants as bt_const
|
|
33
|
+
import beratools.core.tool_base as bt_base
|
|
34
|
+
import beratools.tools.common as bt_common
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CenterlineParams(float, enum.Enum):
|
|
38
|
+
"""
|
|
39
|
+
Parameters for centerline generation.
|
|
40
|
+
|
|
41
|
+
These parameters are used to control the behavior of centerline generation
|
|
42
|
+
and should be adjusted based on the specific requirements of the application.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
BUFFER_CLIP = 5.0
|
|
46
|
+
SEGMENTIZE_LENGTH = 1.0
|
|
47
|
+
SIMPLIFY_LENGTH = 0.5
|
|
48
|
+
SMOOTH_SIGMA = 0.8
|
|
49
|
+
CLEANUP_POLYGON_BY_AREA = 1.0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@enum.unique
|
|
53
|
+
class CenterlineStatus(enum.IntEnum):
|
|
54
|
+
"""
|
|
55
|
+
Status of centerline generation.
|
|
56
|
+
|
|
57
|
+
This enum is used to indicate the status of centerline generation.
|
|
58
|
+
It can be used to track the success or failure of the centerline generation process.
|
|
59
|
+
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
SUCCESS = 1
|
|
63
|
+
FAILED = 2
|
|
64
|
+
REGENERATE_SUCCESS = 3
|
|
65
|
+
REGENERATE_FAILED = 4
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def centerline_is_valid(centerline, input_line):
|
|
69
|
+
"""
|
|
70
|
+
Check if centerline is valid.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
centerline (_type_): _description_
|
|
74
|
+
input_line (sh_geom.LineString): Seed line or least cost path.
|
|
75
|
+
Only two end points are used.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
bool: True if line is valid
|
|
79
|
+
|
|
80
|
+
"""
|
|
81
|
+
if not centerline:
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
# centerline length less the half of least cost path
|
|
85
|
+
if (
|
|
86
|
+
centerline.length < input_line.length / 2
|
|
87
|
+
or centerline.distance(sh_geom.Point(input_line.coords[0])) > bt_const.BT_EPSILON
|
|
88
|
+
or centerline.distance(sh_geom.Point(input_line.coords[-1])) > bt_const.BT_EPSILON
|
|
89
|
+
):
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def snap_end_to_end(in_line, line_reference):
|
|
96
|
+
if type(in_line) is sh_geom.MultiLineString:
|
|
97
|
+
in_line = sh_ops.linemerge(in_line)
|
|
98
|
+
if type(in_line) is sh_geom.MultiLineString:
|
|
99
|
+
print(f"algo_centerline: MultiLineString found {in_line.centroid}, pass.")
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
pts = list(in_line.coords)
|
|
103
|
+
if len(pts) < 2:
|
|
104
|
+
print("snap_end_to_end: input line invalid.")
|
|
105
|
+
return in_line
|
|
106
|
+
|
|
107
|
+
line_start = sh_geom.Point(pts[0])
|
|
108
|
+
line_end = sh_geom.Point(pts[-1])
|
|
109
|
+
ref_ends = sh_geom.MultiPoint([line_reference.coords[0], line_reference.coords[-1]])
|
|
110
|
+
|
|
111
|
+
_, snap_start = sh_ops.nearest_points(line_start, ref_ends)
|
|
112
|
+
_, snap_end = sh_ops.nearest_points(line_end, ref_ends)
|
|
113
|
+
|
|
114
|
+
if in_line.has_z:
|
|
115
|
+
snap_start = shapely.force_3d(snap_start)
|
|
116
|
+
snap_end = shapely.force_3d(snap_end)
|
|
117
|
+
else:
|
|
118
|
+
snap_start = shapely.force_2d(snap_start)
|
|
119
|
+
snap_end = shapely.force_2d(snap_end)
|
|
120
|
+
|
|
121
|
+
pts[0] = snap_start.coords[0]
|
|
122
|
+
pts[-1] = snap_end.coords[0]
|
|
123
|
+
|
|
124
|
+
return sh_geom.LineString(pts)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def find_centerline(poly, input_line):
|
|
128
|
+
"""
|
|
129
|
+
Find centerline from polygon and input line.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
poly : sh_geom.Polygon
|
|
133
|
+
input_line ( sh_geom.LineString): Least cost path or seed line
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
centerline (sh_geom.LineString): Centerline
|
|
137
|
+
status (CenterlineStatus): Status of centerline generation
|
|
138
|
+
|
|
139
|
+
"""
|
|
140
|
+
default_return = input_line, CenterlineStatus.FAILED
|
|
141
|
+
if not poly:
|
|
142
|
+
print("find_centerline: No polygon found")
|
|
143
|
+
return default_return
|
|
144
|
+
|
|
145
|
+
poly = shapely.segmentize(poly, max_segment_length=CenterlineParams.SEGMENTIZE_LENGTH)
|
|
146
|
+
|
|
147
|
+
# buffer to reduce MultiPolygons
|
|
148
|
+
poly = poly.buffer(bt_const.SMALL_BUFFER)
|
|
149
|
+
if type(poly) is sh_geom.MultiPolygon:
|
|
150
|
+
print("sh_geom.MultiPolygon encountered, skip.")
|
|
151
|
+
return default_return
|
|
152
|
+
|
|
153
|
+
exterior_pts = list(poly.exterior.coords)
|
|
154
|
+
|
|
155
|
+
if bt_const.CenterlineFlags.DELETE_HOLES:
|
|
156
|
+
poly = sh_geom.Polygon(exterior_pts)
|
|
157
|
+
if bt_const.CenterlineFlags.SIMPLIFY_POLYGON:
|
|
158
|
+
poly = poly.simplify(CenterlineParams.SIMPLIFY_LENGTH)
|
|
159
|
+
|
|
160
|
+
line_coords = list(input_line.coords)
|
|
161
|
+
|
|
162
|
+
# TODO add more code to filter Voronoi vertices
|
|
163
|
+
src_geom = sh_geom.Point(line_coords[0]).buffer(CenterlineParams.BUFFER_CLIP * 3).intersection(poly)
|
|
164
|
+
dst_geom = sh_geom.Point(line_coords[-1]).buffer(CenterlineParams.BUFFER_CLIP * 3).intersection(poly)
|
|
165
|
+
src_geom = None
|
|
166
|
+
dst_geom = None
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
centerline = get_centerline(
|
|
170
|
+
poly,
|
|
171
|
+
segmentize_maxlen=1,
|
|
172
|
+
max_points=3000,
|
|
173
|
+
simplification=0.05,
|
|
174
|
+
smooth_sigma=CenterlineParams.SMOOTH_SIGMA,
|
|
175
|
+
max_paths=1,
|
|
176
|
+
src_geom=src_geom,
|
|
177
|
+
dst_geom=dst_geom,
|
|
178
|
+
)
|
|
179
|
+
except Exception as e:
|
|
180
|
+
print(f"find_centerline: {e}")
|
|
181
|
+
return default_return
|
|
182
|
+
|
|
183
|
+
if not centerline:
|
|
184
|
+
return default_return
|
|
185
|
+
|
|
186
|
+
if type(centerline) is sh_geom.MultiLineString:
|
|
187
|
+
if len(centerline.geoms) > 1:
|
|
188
|
+
print(" Multiple centerline segments detected, no further processing.")
|
|
189
|
+
return centerline, CenterlineStatus.SUCCESS # TODO: inspect
|
|
190
|
+
elif len(centerline.geoms) == 1:
|
|
191
|
+
centerline = centerline.geoms[0]
|
|
192
|
+
else:
|
|
193
|
+
return default_return
|
|
194
|
+
|
|
195
|
+
cl_coords = list(centerline.coords)
|
|
196
|
+
|
|
197
|
+
# trim centerline at two ends
|
|
198
|
+
head_buffer = sh_geom.Point(cl_coords[0]).buffer(CenterlineParams.BUFFER_CLIP)
|
|
199
|
+
centerline = centerline.difference(head_buffer)
|
|
200
|
+
|
|
201
|
+
end_buffer = sh_geom.Point(cl_coords[-1]).buffer(CenterlineParams.BUFFER_CLIP)
|
|
202
|
+
centerline = centerline.difference(end_buffer)
|
|
203
|
+
|
|
204
|
+
# No centerline detected, use input line instead.
|
|
205
|
+
if not centerline:
|
|
206
|
+
return default_return
|
|
207
|
+
try:
|
|
208
|
+
# Empty centerline detected, use input line instead.
|
|
209
|
+
if centerline.is_empty:
|
|
210
|
+
return default_return
|
|
211
|
+
except Exception as e:
|
|
212
|
+
print(f"find_centerline: {e}")
|
|
213
|
+
|
|
214
|
+
centerline = snap_end_to_end(centerline, input_line)
|
|
215
|
+
|
|
216
|
+
# Check centerline. If valid, regenerate by splitting polygon into two halves.
|
|
217
|
+
if not centerline_is_valid(centerline, input_line):
|
|
218
|
+
try:
|
|
219
|
+
print("Regenerating line ...")
|
|
220
|
+
centerline = regenerate_centerline(poly, input_line)
|
|
221
|
+
return centerline, CenterlineStatus.REGENERATE_SUCCESS
|
|
222
|
+
except Exception as e:
|
|
223
|
+
print(f"find_centerline: {e}")
|
|
224
|
+
return input_line, CenterlineStatus.REGENERATE_FAILED
|
|
225
|
+
|
|
226
|
+
return centerline, CenterlineStatus.SUCCESS
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def find_corridor_polygon(corridor_thresh, in_transform, line_gpd):
|
|
230
|
+
# Threshold corridor raster used for generating centerline
|
|
231
|
+
corridor_thresh_cl = np.ma.where(corridor_thresh == 0.0, 1, 0).data
|
|
232
|
+
if corridor_thresh_cl.dtype == np.int64:
|
|
233
|
+
corridor_thresh_cl = corridor_thresh_cl.astype(np.int32)
|
|
234
|
+
|
|
235
|
+
corridor_mask = np.where(1 == corridor_thresh_cl, True, False)
|
|
236
|
+
poly_generator = rasterio.features.shapes(corridor_thresh_cl, mask=corridor_mask, transform=in_transform)
|
|
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[(line_seg.OLnFID == line_id) & (line_seg.OLnSEG == Seg_id)][
|
|
301
|
+
"geometry"
|
|
302
|
+
].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(input_line, start_dist=0.0, end_dist=input_line.length / 2)
|
|
330
|
+
line_2 = sh_ops.substring(input_line, start_dist=input_line.length / 2, end_dist=input_line.length)
|
|
331
|
+
|
|
332
|
+
pts = shapely.force_2d(
|
|
333
|
+
[
|
|
334
|
+
sh_geom.Point(list(input_line.coords)[0]),
|
|
335
|
+
sh_geom.Point(list(line_1.coords)[-1]),
|
|
336
|
+
sh_geom.Point(list(input_line.coords)[-1]),
|
|
337
|
+
]
|
|
338
|
+
)
|
|
339
|
+
perp = algo_common.generate_perpendicular_line_precise(pts)
|
|
340
|
+
|
|
341
|
+
# sh_geom.MultiPolygon is rare, but need to be dealt with
|
|
342
|
+
# remove polygon of area less than CenterlineParams.CLEANUP_POLYGON_BY_AREA
|
|
343
|
+
poly = poly.buffer(bt_const.SMALL_BUFFER)
|
|
344
|
+
if type(poly) is sh_geom.MultiPolygon:
|
|
345
|
+
poly_geoms = list(poly.geoms)
|
|
346
|
+
poly_valid = [True] * len(poly_geoms)
|
|
347
|
+
for i, item in enumerate(poly_geoms):
|
|
348
|
+
if item.area < CenterlineParams.CLEANUP_POLYGON_BY_AREA:
|
|
349
|
+
poly_valid[i] = False
|
|
350
|
+
|
|
351
|
+
poly_geoms = list(compress(poly_geoms, poly_valid))
|
|
352
|
+
if len(poly_geoms) != 1: # still multi polygon
|
|
353
|
+
print("regenerate_centerline: Multi or none polygon found, pass.")
|
|
354
|
+
|
|
355
|
+
poly = sh_geom.Polygon(poly_geoms[0])
|
|
356
|
+
|
|
357
|
+
poly_exterior = sh_geom.Polygon(poly.buffer(bt_const.SMALL_BUFFER).exterior)
|
|
358
|
+
poly_split = sh_ops.split(poly_exterior, perp)
|
|
359
|
+
|
|
360
|
+
if len(poly_split.geoms) < 2:
|
|
361
|
+
print("regenerate_centerline: polygon sh_ops.split failed, pass.")
|
|
362
|
+
return None
|
|
363
|
+
|
|
364
|
+
poly_1 = poly_split.geoms[0]
|
|
365
|
+
poly_2 = poly_split.geoms[1]
|
|
366
|
+
|
|
367
|
+
# find polygon and line pairs
|
|
368
|
+
pair_line_1 = line_1
|
|
369
|
+
pair_line_2 = line_2
|
|
370
|
+
if not poly_1.intersects(line_1):
|
|
371
|
+
pair_line_1 = line_2
|
|
372
|
+
pair_line_2 = line_1
|
|
373
|
+
elif poly_1.intersection(line_1).length < line_1.length / 3:
|
|
374
|
+
pair_line_1 = line_2
|
|
375
|
+
pair_line_2 = line_1
|
|
376
|
+
|
|
377
|
+
center_line_1 = find_centerline(poly_1, pair_line_1)
|
|
378
|
+
center_line_2 = find_centerline(poly_2, pair_line_2)
|
|
379
|
+
|
|
380
|
+
center_line_1 = center_line_1[0]
|
|
381
|
+
center_line_2 = center_line_2[0]
|
|
382
|
+
|
|
383
|
+
if not center_line_1 or not center_line_2:
|
|
384
|
+
print("Regenerate line: centerline is None")
|
|
385
|
+
return None
|
|
386
|
+
|
|
387
|
+
try:
|
|
388
|
+
if center_line_1.is_empty or center_line_2.is_empty:
|
|
389
|
+
print("Regenerate line: centerline is empty")
|
|
390
|
+
return None
|
|
391
|
+
except Exception as e:
|
|
392
|
+
print(f"regenerate_centerline: {e}")
|
|
393
|
+
|
|
394
|
+
print("Centerline is regenerated.")
|
|
395
|
+
return sh_ops.linemerge(sh_geom.MultiLineString([center_line_1, center_line_2]))
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
class SeedLine:
|
|
399
|
+
"""Class to store seed line and least cost path."""
|
|
400
|
+
|
|
401
|
+
def __init__(self, line_gdf, ras_file, proc_segments, line_radius):
|
|
402
|
+
self.line = line_gdf
|
|
403
|
+
self.raster = ras_file
|
|
404
|
+
self.line_radius = line_radius
|
|
405
|
+
self.lc_path = None
|
|
406
|
+
self.centerline = None
|
|
407
|
+
self.corridor_poly_gpd = None
|
|
408
|
+
|
|
409
|
+
def compute(self):
|
|
410
|
+
line = self.line.geometry[0]
|
|
411
|
+
line_radius = self.line_radius
|
|
412
|
+
in_raster = self.raster
|
|
413
|
+
seed_line = line # LineString
|
|
414
|
+
default_return = (seed_line, seed_line, None)
|
|
415
|
+
|
|
416
|
+
ras_clip, out_meta = bt_common.clip_raster(in_raster, seed_line, line_radius)
|
|
417
|
+
cost_clip, _ = algo_cost.cost_raster(ras_clip, out_meta)
|
|
418
|
+
|
|
419
|
+
lc_path = line
|
|
420
|
+
try:
|
|
421
|
+
if bt_const.CenterlineFlags.USE_SKIMAGE_GRAPH:
|
|
422
|
+
lc_path = bt_dijkstra.find_least_cost_path_skimage(cost_clip, out_meta, seed_line)
|
|
423
|
+
else:
|
|
424
|
+
lc_path = bt_dijkstra.find_least_cost_path(cost_clip, out_meta, seed_line)
|
|
425
|
+
except Exception as e:
|
|
426
|
+
print(e)
|
|
427
|
+
return default_return
|
|
428
|
+
|
|
429
|
+
if lc_path:
|
|
430
|
+
lc_path_coords = lc_path.coords
|
|
431
|
+
else:
|
|
432
|
+
lc_path_coords = []
|
|
433
|
+
|
|
434
|
+
self.lc_path = lc_path
|
|
435
|
+
|
|
436
|
+
# search for centerline
|
|
437
|
+
if len(lc_path_coords) < 2:
|
|
438
|
+
print("No least cost path detected, use input line.")
|
|
439
|
+
self.line["cl_status"] = CenterlineStatus.FAILED.value
|
|
440
|
+
return default_return
|
|
441
|
+
|
|
442
|
+
# get corridor raster
|
|
443
|
+
lc_path = sh_geom.LineString(lc_path_coords)
|
|
444
|
+
ras_clip, out_meta = bt_common.clip_raster(in_raster, lc_path, line_radius * 0.9)
|
|
445
|
+
cost_clip, _ = algo_cost.cost_raster(ras_clip, out_meta)
|
|
446
|
+
|
|
447
|
+
out_transform = out_meta["transform"]
|
|
448
|
+
transformer = rasterio.transform.AffineTransformer(out_transform)
|
|
449
|
+
cell_size = (out_transform[0], -out_transform[4])
|
|
450
|
+
|
|
451
|
+
x1, y1 = lc_path_coords[0]
|
|
452
|
+
x2, y2 = lc_path_coords[-1]
|
|
453
|
+
source = [transformer.rowcol(x1, y1)]
|
|
454
|
+
destination = [transformer.rowcol(x2, y2)]
|
|
455
|
+
corridor_thresh_cl = algo_common.corridor_raster(
|
|
456
|
+
cost_clip,
|
|
457
|
+
out_meta,
|
|
458
|
+
source,
|
|
459
|
+
destination,
|
|
460
|
+
cell_size,
|
|
461
|
+
bt_const.FP_CORRIDOR_THRESHOLD,
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
# find contiguous corridor polygon and extract centerline
|
|
465
|
+
df = gpd.GeoDataFrame(geometry=[seed_line], crs=out_meta["crs"])
|
|
466
|
+
corridor_poly_gpd = find_corridor_polygon(corridor_thresh_cl, out_transform, df)
|
|
467
|
+
center_line, status = find_centerline(corridor_poly_gpd.geometry.iloc[0], lc_path)
|
|
468
|
+
self.line["cl_status"] = status.value
|
|
469
|
+
|
|
470
|
+
self.lc_path = self.line.copy()
|
|
471
|
+
self.lc_path.geometry = [lc_path]
|
|
472
|
+
|
|
473
|
+
self.centerline = self.line.copy()
|
|
474
|
+
self.centerline.geometry = [center_line]
|
|
475
|
+
|
|
476
|
+
self.corridor_poly_gpd = corridor_poly_gpd
|