BERATools 0.2.2__py3-none-any.whl → 0.2.4__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 +8 -3
- beratools/core/{algo_footprint_rel.py → algo_canopy_footprint_exp.py} +176 -139
- beratools/core/algo_centerline.py +61 -77
- beratools/core/algo_common.py +48 -57
- beratools/core/algo_cost.py +18 -25
- beratools/core/algo_dijkstra.py +37 -45
- beratools/core/algo_line_grouping.py +100 -100
- beratools/core/algo_merge_lines.py +40 -8
- beratools/core/algo_split_with_lines.py +289 -304
- beratools/core/algo_vertex_optimization.py +25 -46
- beratools/core/canopy_threshold_relative.py +755 -0
- beratools/core/constants.py +8 -9
- beratools/{tools → core}/line_footprint_functions.py +411 -258
- beratools/core/logger.py +18 -2
- beratools/core/tool_base.py +17 -75
- beratools/gui/assets/BERALogo.ico +0 -0
- beratools/gui/assets/BERA_Splash.gif +0 -0
- beratools/gui/assets/BERA_WizardImage.png +0 -0
- beratools/gui/assets/beratools.json +475 -2171
- beratools/gui/bt_data.py +585 -234
- beratools/gui/bt_gui_main.py +129 -91
- beratools/gui/main.py +4 -7
- beratools/gui/tool_widgets.py +530 -354
- beratools/tools/__init__.py +0 -7
- beratools/tools/{line_footprint_absolute.py → canopy_footprint_absolute.py} +81 -56
- beratools/tools/canopy_footprint_exp.py +113 -0
- beratools/tools/centerline.py +30 -37
- beratools/tools/check_seed_line.py +127 -0
- beratools/tools/common.py +65 -586
- beratools/tools/{line_footprint_fixed.py → ground_footprint.py} +140 -117
- beratools/tools/line_footprint_relative.py +64 -35
- beratools/tools/tool_template.py +48 -40
- beratools/tools/vertex_optimization.py +20 -34
- beratools/utility/env_checks.py +53 -0
- beratools/utility/spatial_common.py +210 -0
- beratools/utility/tool_args.py +138 -0
- beratools-0.2.4.dist-info/METADATA +134 -0
- beratools-0.2.4.dist-info/RECORD +50 -0
- {beratools-0.2.2.dist-info → beratools-0.2.4.dist-info}/WHEEL +1 -1
- beratools-0.2.4.dist-info/entry_points.txt +3 -0
- beratools-0.2.4.dist-info/licenses/LICENSE +674 -0
- beratools/core/algo_tiler.py +0 -428
- beratools/gui/__init__.py +0 -11
- beratools/gui/batch_processing_dlg.py +0 -513
- beratools/gui/map_window.py +0 -162
- beratools/tools/Beratools_r_script.r +0 -1120
- beratools/tools/Ht_metrics.py +0 -116
- beratools/tools/batch_processing.py +0 -136
- beratools/tools/canopy_threshold_relative.py +0 -672
- beratools/tools/canopycostraster.py +0 -222
- beratools/tools/fl_regen_csf.py +0 -428
- beratools/tools/forest_line_attributes.py +0 -408
- beratools/tools/line_grouping.py +0 -45
- beratools/tools/ln_relative_metrics.py +0 -615
- beratools/tools/r_cal_lpi_elai.r +0 -25
- beratools/tools/r_generate_pd_focalraster.r +0 -101
- beratools/tools/r_interface.py +0 -80
- beratools/tools/r_point_density.r +0 -9
- beratools/tools/rpy_chm2trees.py +0 -86
- beratools/tools/rpy_dsm_chm_by.py +0 -81
- beratools/tools/rpy_dtm_by.py +0 -63
- beratools/tools/rpy_find_cellsize.py +0 -43
- beratools/tools/rpy_gnd_csf.py +0 -74
- beratools/tools/rpy_hummock_hollow.py +0 -85
- beratools/tools/rpy_hummock_hollow_raster.py +0 -71
- beratools/tools/rpy_las_info.py +0 -51
- beratools/tools/rpy_laz2las.py +0 -40
- beratools/tools/rpy_lpi_elai_lascat.py +0 -466
- beratools/tools/rpy_normalized_lidar_by.py +0 -56
- beratools/tools/rpy_percent_above_dbh.py +0 -80
- beratools/tools/rpy_points2trees.py +0 -88
- beratools/tools/rpy_vegcoverage.py +0 -94
- beratools/tools/tiler.py +0 -48
- beratools/tools/zonal_threshold.py +0 -144
- beratools-0.2.2.dist-info/METADATA +0 -108
- beratools-0.2.2.dist-info/RECORD +0 -74
- beratools-0.2.2.dist-info/entry_points.txt +0 -2
- beratools-0.2.2.dist-info/licenses/LICENSE +0 -22
|
@@ -10,9 +10,10 @@ Description:
|
|
|
10
10
|
This script is part of the BERA Tools.
|
|
11
11
|
Webpage: https://github.com/appliedgrg/beratools
|
|
12
12
|
|
|
13
|
-
This file is intended to be hosting algorithms and utility functions/classes
|
|
13
|
+
This file is intended to be hosting algorithms and utility functions/classes
|
|
14
14
|
for centerline tool.
|
|
15
15
|
"""
|
|
16
|
+
|
|
16
17
|
import enum
|
|
17
18
|
from itertools import compress
|
|
18
19
|
|
|
@@ -30,23 +31,40 @@ import beratools.core.algo_cost as algo_cost
|
|
|
30
31
|
import beratools.core.algo_dijkstra as bt_dijkstra
|
|
31
32
|
import beratools.core.constants as bt_const
|
|
32
33
|
import beratools.core.tool_base as bt_base
|
|
33
|
-
import beratools.
|
|
34
|
+
import beratools.utility.spatial_common as sp_common
|
|
34
35
|
|
|
35
36
|
|
|
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
|
+
|
|
37
45
|
BUFFER_CLIP = 5.0
|
|
38
46
|
SEGMENTIZE_LENGTH = 1.0
|
|
39
47
|
SIMPLIFY_LENGTH = 0.5
|
|
40
48
|
SMOOTH_SIGMA = 0.8
|
|
41
49
|
CLEANUP_POLYGON_BY_AREA = 1.0
|
|
42
50
|
|
|
51
|
+
|
|
43
52
|
@enum.unique
|
|
44
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
|
+
|
|
45
62
|
SUCCESS = 1
|
|
46
63
|
FAILED = 2
|
|
47
64
|
REGENERATE_SUCCESS = 3
|
|
48
65
|
REGENERATE_FAILED = 4
|
|
49
66
|
|
|
67
|
+
|
|
50
68
|
def centerline_is_valid(centerline, input_line):
|
|
51
69
|
"""
|
|
52
70
|
Check if centerline is valid.
|
|
@@ -66,10 +84,8 @@ def centerline_is_valid(centerline, input_line):
|
|
|
66
84
|
# centerline length less the half of least cost path
|
|
67
85
|
if (
|
|
68
86
|
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
|
|
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
|
|
73
89
|
):
|
|
74
90
|
return False
|
|
75
91
|
|
|
@@ -80,12 +96,12 @@ def snap_end_to_end(in_line, line_reference):
|
|
|
80
96
|
if type(in_line) is sh_geom.MultiLineString:
|
|
81
97
|
in_line = sh_ops.linemerge(in_line)
|
|
82
98
|
if type(in_line) is sh_geom.MultiLineString:
|
|
83
|
-
print(f
|
|
99
|
+
print(f"algo_centerline: MultiLineString found {in_line.centroid}, pass.")
|
|
84
100
|
return None
|
|
85
101
|
|
|
86
102
|
pts = list(in_line.coords)
|
|
87
103
|
if len(pts) < 2:
|
|
88
|
-
print(
|
|
104
|
+
print("snap_end_to_end: input line invalid.")
|
|
89
105
|
return in_line
|
|
90
106
|
|
|
91
107
|
line_start = sh_geom.Point(pts[0])
|
|
@@ -123,17 +139,15 @@ def find_centerline(poly, input_line):
|
|
|
123
139
|
"""
|
|
124
140
|
default_return = input_line, CenterlineStatus.FAILED
|
|
125
141
|
if not poly:
|
|
126
|
-
print(
|
|
142
|
+
print("find_centerline: No polygon found")
|
|
127
143
|
return default_return
|
|
128
144
|
|
|
129
|
-
poly = shapely.segmentize(
|
|
130
|
-
poly, max_segment_length=CenterlineParams.SEGMENTIZE_LENGTH
|
|
131
|
-
)
|
|
145
|
+
poly = shapely.segmentize(poly, max_segment_length=CenterlineParams.SEGMENTIZE_LENGTH)
|
|
132
146
|
|
|
133
147
|
# buffer to reduce MultiPolygons
|
|
134
|
-
poly = poly.buffer(bt_const.SMALL_BUFFER)
|
|
148
|
+
poly = poly.buffer(bt_const.SMALL_BUFFER)
|
|
135
149
|
if type(poly) is sh_geom.MultiPolygon:
|
|
136
|
-
print(
|
|
150
|
+
print("sh_geom.MultiPolygon encountered, skip.")
|
|
137
151
|
return default_return
|
|
138
152
|
|
|
139
153
|
exterior_pts = list(poly.exterior.coords)
|
|
@@ -146,16 +160,8 @@ def find_centerline(poly, input_line):
|
|
|
146
160
|
line_coords = list(input_line.coords)
|
|
147
161
|
|
|
148
162
|
# TODO add more code to filter Voronoi vertices
|
|
149
|
-
src_geom = (
|
|
150
|
-
|
|
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
|
-
)
|
|
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)
|
|
159
165
|
src_geom = None
|
|
160
166
|
dst_geom = None
|
|
161
167
|
|
|
@@ -171,7 +177,7 @@ def find_centerline(poly, input_line):
|
|
|
171
177
|
dst_geom=dst_geom,
|
|
172
178
|
)
|
|
173
179
|
except Exception as e:
|
|
174
|
-
print(f
|
|
180
|
+
print(f"find_centerline: {e}")
|
|
175
181
|
return default_return
|
|
176
182
|
|
|
177
183
|
if not centerline:
|
|
@@ -189,14 +195,10 @@ def find_centerline(poly, input_line):
|
|
|
189
195
|
cl_coords = list(centerline.coords)
|
|
190
196
|
|
|
191
197
|
# trim centerline at two ends
|
|
192
|
-
head_buffer = sh_geom.Point(cl_coords[0]).buffer(
|
|
193
|
-
CenterlineParams.BUFFER_CLIP
|
|
194
|
-
)
|
|
198
|
+
head_buffer = sh_geom.Point(cl_coords[0]).buffer(CenterlineParams.BUFFER_CLIP)
|
|
195
199
|
centerline = centerline.difference(head_buffer)
|
|
196
200
|
|
|
197
|
-
end_buffer = sh_geom.Point(cl_coords[-1]).buffer(
|
|
198
|
-
CenterlineParams.BUFFER_CLIP
|
|
199
|
-
)
|
|
201
|
+
end_buffer = sh_geom.Point(cl_coords[-1]).buffer(CenterlineParams.BUFFER_CLIP)
|
|
200
202
|
centerline = centerline.difference(end_buffer)
|
|
201
203
|
|
|
202
204
|
# No centerline detected, use input line instead.
|
|
@@ -207,18 +209,18 @@ def find_centerline(poly, input_line):
|
|
|
207
209
|
if centerline.is_empty:
|
|
208
210
|
return default_return
|
|
209
211
|
except Exception as e:
|
|
210
|
-
print(f
|
|
212
|
+
print(f"find_centerline: {e}")
|
|
211
213
|
|
|
212
214
|
centerline = snap_end_to_end(centerline, input_line)
|
|
213
215
|
|
|
214
216
|
# Check centerline. If valid, regenerate by splitting polygon into two halves.
|
|
215
217
|
if not centerline_is_valid(centerline, input_line):
|
|
216
218
|
try:
|
|
217
|
-
print(
|
|
219
|
+
print("Regenerating line ...")
|
|
218
220
|
centerline = regenerate_centerline(poly, input_line)
|
|
219
221
|
return centerline, CenterlineStatus.REGENERATE_SUCCESS
|
|
220
222
|
except Exception as e:
|
|
221
|
-
print(f
|
|
223
|
+
print(f"find_centerline: {e}")
|
|
222
224
|
return input_line, CenterlineStatus.REGENERATE_FAILED
|
|
223
225
|
|
|
224
226
|
return centerline, CenterlineStatus.SUCCESS
|
|
@@ -231,9 +233,7 @@ def find_corridor_polygon(corridor_thresh, in_transform, line_gpd):
|
|
|
231
233
|
corridor_thresh_cl = corridor_thresh_cl.astype(np.int32)
|
|
232
234
|
|
|
233
235
|
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
|
-
)
|
|
236
|
+
poly_generator = rasterio.features.shapes(corridor_thresh_cl, mask=corridor_mask, transform=in_transform)
|
|
237
237
|
corridor_polygon = []
|
|
238
238
|
|
|
239
239
|
try:
|
|
@@ -244,7 +244,7 @@ def find_corridor_polygon(corridor_thresh, in_transform, line_gpd):
|
|
|
244
244
|
print(f"find_corridor_polygon: {e}")
|
|
245
245
|
|
|
246
246
|
if corridor_polygon:
|
|
247
|
-
corridor_polygon =
|
|
247
|
+
corridor_polygon = sh_ops.unary_union(corridor_polygon)
|
|
248
248
|
if type(corridor_polygon) is sh_geom.MultiPolygon:
|
|
249
249
|
poly_list = shapely.get_parts(corridor_polygon)
|
|
250
250
|
merge_poly = poly_list[0]
|
|
@@ -283,7 +283,7 @@ def process_single_centerline(row_and_path):
|
|
|
283
283
|
|
|
284
284
|
poly = row.geometry.iloc[0]
|
|
285
285
|
centerline, status = find_centerline(poly, lc_path)
|
|
286
|
-
row[
|
|
286
|
+
row["centerline"] = centerline
|
|
287
287
|
|
|
288
288
|
return row
|
|
289
289
|
|
|
@@ -295,14 +295,14 @@ def find_centerlines(poly_gpd, line_seg, processes):
|
|
|
295
295
|
try:
|
|
296
296
|
for i in poly_gpd.index:
|
|
297
297
|
row = poly_gpd.loc[[i]]
|
|
298
|
-
if
|
|
299
|
-
line_id, Seg_id = row[
|
|
300
|
-
lc_path = line_seg.loc[
|
|
301
|
-
|
|
302
|
-
]
|
|
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
303
|
else:
|
|
304
|
-
line_id = row[
|
|
305
|
-
lc_path = line_seg.loc[(line_seg.OLnFID == line_id)][
|
|
304
|
+
line_id = row["OLnFID"].iloc[0]
|
|
305
|
+
lc_path = line_seg.loc[(line_seg.OLnFID == line_id)]["geometry"].iloc[0]
|
|
306
306
|
|
|
307
307
|
rows_and_paths.append((row, lc_path))
|
|
308
308
|
except Exception as e:
|
|
@@ -326,12 +326,8 @@ def regenerate_centerline(poly, input_line):
|
|
|
326
326
|
sh_geom.MultiLineString
|
|
327
327
|
|
|
328
328
|
"""
|
|
329
|
-
line_1 = sh_ops.substring(
|
|
330
|
-
|
|
331
|
-
)
|
|
332
|
-
line_2 = sh_ops.substring(
|
|
333
|
-
input_line, start_dist=input_line.length / 2, end_dist=input_line.length
|
|
334
|
-
)
|
|
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)
|
|
335
331
|
|
|
336
332
|
pts = shapely.force_2d(
|
|
337
333
|
[
|
|
@@ -358,9 +354,7 @@ def regenerate_centerline(poly, input_line):
|
|
|
358
354
|
|
|
359
355
|
poly = sh_geom.Polygon(poly_geoms[0])
|
|
360
356
|
|
|
361
|
-
poly_exterior = sh_geom.Polygon(
|
|
362
|
-
poly.buffer(bt_const.SMALL_BUFFER).exterior
|
|
363
|
-
)
|
|
357
|
+
poly_exterior = sh_geom.Polygon(poly.buffer(bt_const.SMALL_BUFFER).exterior)
|
|
364
358
|
poly_split = sh_ops.split(poly_exterior, perp)
|
|
365
359
|
|
|
366
360
|
if len(poly_split.geoms) < 2:
|
|
@@ -400,6 +394,7 @@ def regenerate_centerline(poly, input_line):
|
|
|
400
394
|
print("Centerline is regenerated.")
|
|
401
395
|
return sh_ops.linemerge(sh_geom.MultiLineString([center_line_1, center_line_2]))
|
|
402
396
|
|
|
397
|
+
|
|
403
398
|
class SeedLine:
|
|
404
399
|
"""Class to store seed line and least cost path."""
|
|
405
400
|
|
|
@@ -416,24 +411,19 @@ class SeedLine:
|
|
|
416
411
|
line_radius = self.line_radius
|
|
417
412
|
in_raster = self.raster
|
|
418
413
|
seed_line = line # LineString
|
|
419
|
-
default_return = (seed_line, seed_line, None)
|
|
420
414
|
|
|
421
|
-
ras_clip, out_meta =
|
|
415
|
+
ras_clip, out_meta = sp_common.clip_raster(in_raster, seed_line, line_radius)
|
|
422
416
|
cost_clip, _ = algo_cost.cost_raster(ras_clip, out_meta)
|
|
423
417
|
|
|
424
418
|
lc_path = line
|
|
425
419
|
try:
|
|
426
420
|
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
|
-
)
|
|
421
|
+
lc_path = bt_dijkstra.find_least_cost_path_skimage(cost_clip, out_meta, seed_line)
|
|
430
422
|
else:
|
|
431
|
-
lc_path = bt_dijkstra.find_least_cost_path(
|
|
432
|
-
cost_clip, out_meta, seed_line
|
|
433
|
-
)
|
|
423
|
+
lc_path = bt_dijkstra.find_least_cost_path(cost_clip, out_meta, seed_line)
|
|
434
424
|
except Exception as e:
|
|
435
425
|
print(e)
|
|
436
|
-
return
|
|
426
|
+
return
|
|
437
427
|
|
|
438
428
|
if lc_path:
|
|
439
429
|
lc_path_coords = lc_path.coords
|
|
@@ -445,14 +435,12 @@ class SeedLine:
|
|
|
445
435
|
# search for centerline
|
|
446
436
|
if len(lc_path_coords) < 2:
|
|
447
437
|
print("No least cost path detected, use input line.")
|
|
448
|
-
self.line["
|
|
449
|
-
return
|
|
438
|
+
self.line["cl_status"] = CenterlineStatus.FAILED.value
|
|
439
|
+
return
|
|
450
440
|
|
|
451
441
|
# get corridor raster
|
|
452
442
|
lc_path = sh_geom.LineString(lc_path_coords)
|
|
453
|
-
ras_clip, out_meta =
|
|
454
|
-
in_raster, lc_path, line_radius * 0.9
|
|
455
|
-
)
|
|
443
|
+
ras_clip, out_meta = sp_common.clip_raster(in_raster, lc_path, line_radius * 0.9)
|
|
456
444
|
cost_clip, _ = algo_cost.cost_raster(ras_clip, out_meta)
|
|
457
445
|
|
|
458
446
|
out_transform = out_meta["transform"]
|
|
@@ -474,13 +462,9 @@ class SeedLine:
|
|
|
474
462
|
|
|
475
463
|
# find contiguous corridor polygon and extract centerline
|
|
476
464
|
df = gpd.GeoDataFrame(geometry=[seed_line], crs=out_meta["crs"])
|
|
477
|
-
corridor_poly_gpd = find_corridor_polygon(
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
center_line, status = find_centerline(
|
|
481
|
-
corridor_poly_gpd.geometry.iloc[0], lc_path
|
|
482
|
-
)
|
|
483
|
-
self.line["status"] = status.value
|
|
465
|
+
corridor_poly_gpd = find_corridor_polygon(corridor_thresh_cl, out_transform, df)
|
|
466
|
+
center_line, status = find_centerline(corridor_poly_gpd.geometry.iloc[0], lc_path)
|
|
467
|
+
self.line["cl_status"] = status.value
|
|
484
468
|
|
|
485
469
|
self.lc_path = self.line.copy()
|
|
486
470
|
self.lc_path.geometry = [lc_path]
|
|
@@ -488,4 +472,4 @@ class SeedLine:
|
|
|
488
472
|
self.centerline = self.line.copy()
|
|
489
473
|
self.centerline.geometry = [center_line]
|
|
490
474
|
|
|
491
|
-
self.corridor_poly_gpd = corridor_poly_gpd
|
|
475
|
+
self.corridor_poly_gpd = corridor_poly_gpd
|
beratools/core/algo_common.py
CHANGED
|
@@ -13,6 +13,7 @@ Description:
|
|
|
13
13
|
The purpose of this script is to provide common algorithms
|
|
14
14
|
and utility functions/classes.
|
|
15
15
|
"""
|
|
16
|
+
|
|
16
17
|
import math
|
|
17
18
|
import tempfile
|
|
18
19
|
from pathlib import Path
|
|
@@ -32,8 +33,10 @@ from scipy import ndimage
|
|
|
32
33
|
import beratools.core.algo_cost as algo_cost
|
|
33
34
|
import beratools.core.constants as bt_const
|
|
34
35
|
|
|
36
|
+
gpd.options.io_engine = "pyogrio"
|
|
35
37
|
DISTANCE_THRESHOLD = 2 # 1 meter for intersection neighborhood
|
|
36
38
|
|
|
39
|
+
|
|
37
40
|
def process_single_item(cls_obj):
|
|
38
41
|
"""
|
|
39
42
|
Process a class object for universal multiprocessing.
|
|
@@ -45,8 +48,16 @@ def process_single_item(cls_obj):
|
|
|
45
48
|
cls_obj: Class object after processing
|
|
46
49
|
|
|
47
50
|
"""
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
try:
|
|
52
|
+
cls_obj.compute()
|
|
53
|
+
return cls_obj
|
|
54
|
+
except Exception as e:
|
|
55
|
+
import traceback
|
|
56
|
+
|
|
57
|
+
print(f"❌ Exception during compute() for object: {e}")
|
|
58
|
+
traceback.print_exc()
|
|
59
|
+
return None
|
|
60
|
+
|
|
50
61
|
|
|
51
62
|
def read_geospatial_file(file_path, layer=None):
|
|
52
63
|
"""
|
|
@@ -64,12 +75,11 @@ def read_geospatial_file(file_path, layer=None):
|
|
|
64
75
|
|
|
65
76
|
"""
|
|
66
77
|
try:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
gdf = gpd.read_file(file_path, layer=layer)
|
|
78
|
+
kwargs = {}
|
|
79
|
+
if layer is not None:
|
|
80
|
+
kwargs['layer'] = layer
|
|
81
|
+
|
|
82
|
+
gdf = gpd.read_file(file_path, **kwargs)
|
|
73
83
|
|
|
74
84
|
# Clean the geometries in the GeoDataFrame
|
|
75
85
|
gdf = clean_geometries(gdf)
|
|
@@ -80,12 +90,14 @@ def read_geospatial_file(file_path, layer=None):
|
|
|
80
90
|
print(f"Error reading file {file_path}: {e}")
|
|
81
91
|
return None
|
|
82
92
|
|
|
93
|
+
|
|
83
94
|
def has_multilinestring(gdf):
|
|
84
95
|
"""Check if any geometry is a MultiLineString."""
|
|
85
96
|
# Filter out None values (invalid geometries) from the GeoDataFrame
|
|
86
97
|
valid_geometries = gdf.geometry
|
|
87
98
|
return any(isinstance(geom, sh_geom.MultiLineString) for geom in valid_geometries)
|
|
88
99
|
|
|
100
|
+
|
|
89
101
|
def clean_geometries(gdf):
|
|
90
102
|
"""
|
|
91
103
|
Remove rows with invalid, None, or empty geometries from the GeoDataFrame.
|
|
@@ -101,26 +113,23 @@ def clean_geometries(gdf):
|
|
|
101
113
|
# Remove rows where the geometry is invalid, None, or empty
|
|
102
114
|
gdf = gdf[gdf.geometry.is_valid] # Only keep valid geometries
|
|
103
115
|
gdf = gdf[~gdf.geometry.isna()] # Remove rows with None geometries
|
|
104
|
-
gdf = gdf[
|
|
105
|
-
gdf.geometry.apply(lambda geom: not geom.is_empty)
|
|
106
|
-
] # Remove empty geometries
|
|
116
|
+
gdf = gdf[gdf.geometry.apply(lambda geom: not geom.is_empty)] # Remove empty geometries
|
|
107
117
|
return gdf
|
|
108
118
|
|
|
119
|
+
|
|
109
120
|
def clean_line_geometries(line_gdf):
|
|
110
121
|
"""Clean line geometries in the GeoDataFrame."""
|
|
111
122
|
if line_gdf is None:
|
|
112
123
|
return line_gdf
|
|
113
|
-
|
|
124
|
+
|
|
114
125
|
if line_gdf.empty:
|
|
115
126
|
return line_gdf
|
|
116
|
-
|
|
117
|
-
line_gdf = line_gdf[
|
|
118
|
-
~line_gdf.geometry.isna()
|
|
119
|
-
& ~line_gdf.geometry.is_empty
|
|
120
|
-
]
|
|
127
|
+
|
|
128
|
+
line_gdf = line_gdf[~line_gdf.geometry.isna() & ~line_gdf.geometry.is_empty]
|
|
121
129
|
line_gdf = line_gdf[line_gdf.geometry.length > bt_const.SMALL_BUFFER]
|
|
122
130
|
return line_gdf
|
|
123
131
|
|
|
132
|
+
|
|
124
133
|
def prepare_lines_gdf(file_path, layer=None, proc_segments=True):
|
|
125
134
|
"""
|
|
126
135
|
Split lines at vertices or return original rows.
|
|
@@ -149,22 +158,14 @@ def prepare_lines_gdf(file_path, layer=None, proc_segments=True):
|
|
|
149
158
|
segment = sh_geom.LineString([coords[i], coords[i + 1]])
|
|
150
159
|
|
|
151
160
|
# Copy over all non-geometry columns (excluding 'geometry')
|
|
152
|
-
attributes = {
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
single_row_gdf = gpd.GeoDataFrame(
|
|
156
|
-
[attributes], geometry=[segment], crs=gdf.crs
|
|
157
|
-
)
|
|
161
|
+
attributes = {col: getattr(row, col) for col in gdf.columns if col != "geometry"}
|
|
162
|
+
single_row_gdf = gpd.GeoDataFrame([attributes], geometry=[segment], crs=gdf.crs)
|
|
158
163
|
split_gdf_list.append(single_row_gdf)
|
|
159
164
|
|
|
160
165
|
else:
|
|
161
166
|
# If not proc_segment, add the original row as a single-row GeoDataFrame
|
|
162
|
-
attributes = {
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
single_row_gdf = gpd.GeoDataFrame(
|
|
166
|
-
[attributes], geometry=[line], crs=gdf.crs
|
|
167
|
-
)
|
|
167
|
+
attributes = {col: getattr(row, col) for col in gdf.columns if col != "geometry"}
|
|
168
|
+
single_row_gdf = gpd.GeoDataFrame([attributes], geometry=[line], crs=gdf.crs)
|
|
168
169
|
split_gdf_list.append(single_row_gdf)
|
|
169
170
|
|
|
170
171
|
return split_gdf_list
|
|
@@ -180,15 +181,11 @@ def morph_raster(corridor_thresh, canopy_raster, exp_shk_cell, cell_size_x):
|
|
|
180
181
|
# Process: Expand
|
|
181
182
|
# FLM original Expand equivalent
|
|
182
183
|
cell_size = int(exp_shk_cell * 2 + 1)
|
|
183
|
-
expanded = ndimage.grey_dilation(
|
|
184
|
-
raster_class, size=(cell_size, cell_size)
|
|
185
|
-
)
|
|
184
|
+
expanded = ndimage.grey_dilation(raster_class, size=(cell_size, cell_size))
|
|
186
185
|
|
|
187
186
|
# Process: Shrink
|
|
188
187
|
# FLM original Shrink equivalent
|
|
189
|
-
file_shrink = ndimage.grey_erosion(
|
|
190
|
-
expanded, size=(cell_size, cell_size)
|
|
191
|
-
)
|
|
188
|
+
file_shrink = ndimage.grey_erosion(expanded, size=(cell_size, cell_size))
|
|
192
189
|
|
|
193
190
|
else:
|
|
194
191
|
if bt_const.BT_DEBUGGING:
|
|
@@ -250,6 +247,7 @@ def intersection_of_lines(line_1, line_2):
|
|
|
250
247
|
|
|
251
248
|
return inter
|
|
252
249
|
|
|
250
|
+
|
|
253
251
|
def get_angle(line, vertex_index):
|
|
254
252
|
"""
|
|
255
253
|
Calculate the angle of the first or last segment.
|
|
@@ -287,15 +285,14 @@ def get_angle(line, vertex_index):
|
|
|
287
285
|
|
|
288
286
|
return angle
|
|
289
287
|
|
|
288
|
+
|
|
290
289
|
def points_are_close(pt1, pt2):
|
|
291
|
-
if (
|
|
292
|
-
abs(pt1.x - pt2.x) < DISTANCE_THRESHOLD
|
|
293
|
-
and abs(pt1.y - pt2.y) < DISTANCE_THRESHOLD
|
|
294
|
-
):
|
|
290
|
+
if abs(pt1.x - pt2.x) < DISTANCE_THRESHOLD and abs(pt1.y - pt2.y) < DISTANCE_THRESHOLD:
|
|
295
291
|
return True
|
|
296
292
|
else:
|
|
297
293
|
return False
|
|
298
294
|
|
|
295
|
+
|
|
299
296
|
def generate_raster_footprint(in_raster, latlon=True):
|
|
300
297
|
inter_img = "image_overview.tif"
|
|
301
298
|
|
|
@@ -331,6 +328,7 @@ def generate_raster_footprint(in_raster, latlon=True):
|
|
|
331
328
|
|
|
332
329
|
return geom
|
|
333
330
|
|
|
331
|
+
|
|
334
332
|
def save_raster_to_file(in_raster_mem, in_meta, out_raster_file):
|
|
335
333
|
"""
|
|
336
334
|
Save raster matrix in memory to file.
|
|
@@ -344,6 +342,7 @@ def save_raster_to_file(in_raster_mem, in_meta, out_raster_file):
|
|
|
344
342
|
with rasterio.open(out_raster_file, "w", **in_meta) as dest:
|
|
345
343
|
dest.write(in_raster_mem, indexes=1)
|
|
346
344
|
|
|
345
|
+
|
|
347
346
|
def generate_perpendicular_line_precise(points, offset=20):
|
|
348
347
|
"""
|
|
349
348
|
Generate a perpendicular line to the input line at the given point.
|
|
@@ -359,7 +358,7 @@ def generate_perpendicular_line_precise(points, offset=20):
|
|
|
359
358
|
# Compute the angle of the line
|
|
360
359
|
if len(points) not in [2, 3]:
|
|
361
360
|
return None
|
|
362
|
-
|
|
361
|
+
|
|
363
362
|
center = points[1]
|
|
364
363
|
perp_line = None
|
|
365
364
|
|
|
@@ -379,9 +378,7 @@ def generate_perpendicular_line_precise(points, offset=20):
|
|
|
379
378
|
start = [center.x + offset / 2.0, center.y]
|
|
380
379
|
end = [center.x - offset / 2.0, center.y]
|
|
381
380
|
line = sh_geom.LineString([start, end])
|
|
382
|
-
perp_line = sh_aff.rotate(
|
|
383
|
-
line, angle + math.pi / 2.0, origin=center, use_radians=True
|
|
384
|
-
)
|
|
381
|
+
perp_line = sh_aff.rotate(line, angle + math.pi / 2.0, origin=center, use_radians=True)
|
|
385
382
|
elif len(points) == 3:
|
|
386
383
|
head = points[0]
|
|
387
384
|
tail = points[2]
|
|
@@ -397,15 +394,9 @@ def generate_perpendicular_line_precise(points, offset=20):
|
|
|
397
394
|
head_new = shapely.force_3d(head_new)
|
|
398
395
|
try:
|
|
399
396
|
perp_seg_1 = sh_geom.LineString([center, head_new])
|
|
400
|
-
perp_seg_1 = sh_aff.rotate(
|
|
401
|
-
|
|
402
|
-
)
|
|
403
|
-
perp_seg_2 = sh_aff.rotate(
|
|
404
|
-
perp_seg_1, math.pi, origin=center, use_radians=True
|
|
405
|
-
)
|
|
406
|
-
perp_line = sh_geom.LineString(
|
|
407
|
-
[list(perp_seg_1.coords)[1], list(perp_seg_2.coords)[1]]
|
|
408
|
-
)
|
|
397
|
+
perp_seg_1 = sh_aff.rotate(perp_seg_1, angle_diff, origin=center, use_radians=True)
|
|
398
|
+
perp_seg_2 = sh_aff.rotate(perp_seg_1, math.pi, origin=center, use_radians=True)
|
|
399
|
+
perp_line = sh_geom.LineString([list(perp_seg_1.coords)[1], list(perp_seg_2.coords)[1]])
|
|
409
400
|
except Exception as e:
|
|
410
401
|
print(e)
|
|
411
402
|
|
|
@@ -426,9 +417,8 @@ def _line_angle(point_1, point_2):
|
|
|
426
417
|
angle = math.atan2(delta_y, delta_x)
|
|
427
418
|
return angle
|
|
428
419
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
):
|
|
420
|
+
|
|
421
|
+
def corridor_raster(raster_clip, out_meta, source, destination, cell_size, corridor_threshold):
|
|
432
422
|
"""
|
|
433
423
|
Calculate corridor raster.
|
|
434
424
|
|
|
@@ -442,7 +432,7 @@ def corridor_raster(
|
|
|
442
432
|
|
|
443
433
|
Returns:
|
|
444
434
|
corridor raster
|
|
445
|
-
|
|
435
|
+
|
|
446
436
|
"""
|
|
447
437
|
try:
|
|
448
438
|
# change all nan to BT_NODATA_COST for workaround
|
|
@@ -481,6 +471,7 @@ def corridor_raster(
|
|
|
481
471
|
|
|
482
472
|
return corridor_thresh_cl
|
|
483
473
|
|
|
474
|
+
|
|
484
475
|
def remove_holes(geom):
|
|
485
476
|
if geom.geom_type == "Polygon":
|
|
486
477
|
if geom.interiors:
|
|
@@ -494,4 +485,4 @@ def remove_holes(geom):
|
|
|
494
485
|
else:
|
|
495
486
|
new_polygons.append(polygon)
|
|
496
487
|
return sh_geom.MultiPolygon(new_polygons)
|
|
497
|
-
return geom # Return other geometry types as is
|
|
488
|
+
return geom # Return other geometry types as is
|