BERATools 0.2.3__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.3.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.3.dist-info/METADATA +0 -108
- beratools-0.2.3.dist-info/RECORD +0 -74
- beratools-0.2.3.dist-info/entry_points.txt +0 -2
- beratools-0.2.3.dist-info/licenses/LICENSE +0 -22
|
@@ -10,28 +10,38 @@ Description:
|
|
|
10
10
|
This script is part of the BERA Tools.
|
|
11
11
|
Webpage: https://github.com/appliedgrg/beratools
|
|
12
12
|
|
|
13
|
-
This file hosts the
|
|
13
|
+
This file hosts the ground_footprint tool.
|
|
14
14
|
"""
|
|
15
|
+
|
|
16
|
+
import logging
|
|
15
17
|
import math
|
|
16
|
-
import time
|
|
17
18
|
from itertools import chain
|
|
18
19
|
from pathlib import Path
|
|
19
20
|
|
|
20
21
|
import geopandas as gpd
|
|
21
22
|
import numpy as np
|
|
22
23
|
import pandas as pd
|
|
24
|
+
import pyogrio.errors
|
|
23
25
|
import shapely.geometry as sh_geom
|
|
24
26
|
import shapely.ops as sh_ops
|
|
25
27
|
|
|
26
28
|
import beratools.core.algo_common as algo_common
|
|
27
29
|
import beratools.core.constants as bt_const
|
|
28
|
-
import beratools.
|
|
30
|
+
import beratools.utility.spatial_common as sp_common
|
|
29
31
|
from beratools.core.algo_line_grouping import LineGrouping
|
|
32
|
+
from beratools.core.algo_merge_lines import custom_line_merge
|
|
30
33
|
from beratools.core.algo_split_with_lines import LineSplitter
|
|
34
|
+
from beratools.core.logger import Logger
|
|
31
35
|
from beratools.core.tool_base import execute_multiprocessing
|
|
36
|
+
from beratools.utility.tool_args import CallMode
|
|
37
|
+
|
|
38
|
+
log = Logger("ground_footprint", file_level=logging.INFO)
|
|
39
|
+
logger = log.get_logger()
|
|
40
|
+
print = log.print
|
|
32
41
|
|
|
33
42
|
FP_FIXED_WIDTH_DEFAULT = 5.0
|
|
34
43
|
|
|
44
|
+
|
|
35
45
|
def prepare_line_args(line_gdf, poly_gdf, n_samples, offset, width_percentile):
|
|
36
46
|
"""
|
|
37
47
|
Generate arguments for each line in the GeoDataFrame.
|
|
@@ -65,14 +75,10 @@ def prepare_line_args(line_gdf, poly_gdf, n_samples, offset, width_percentile):
|
|
|
65
75
|
|
|
66
76
|
inter_poly = poly_gdf.loc[spatial_index.query(line)]
|
|
67
77
|
if bt_const.BT_GROUP in inter_poly.columns:
|
|
68
|
-
inter_poly = inter_poly[
|
|
69
|
-
inter_poly[bt_const.BT_GROUP] == row[bt_const.BT_GROUP].values[0]
|
|
70
|
-
]
|
|
78
|
+
inter_poly = inter_poly[inter_poly[bt_const.BT_GROUP] == row[bt_const.BT_GROUP].values[0]]
|
|
71
79
|
|
|
72
80
|
try:
|
|
73
|
-
line_args.append(
|
|
74
|
-
[row, inter_poly, n_samples, offset, width_percentile]
|
|
75
|
-
)
|
|
81
|
+
line_args.append([row, inter_poly, n_samples, offset, width_percentile])
|
|
76
82
|
except Exception as e:
|
|
77
83
|
print(e)
|
|
78
84
|
|
|
@@ -152,7 +158,7 @@ def process_single_line(line_arg):
|
|
|
152
158
|
def generate_fixed_width_footprint(line_gdf, max_width=False):
|
|
153
159
|
"""
|
|
154
160
|
Create a buffer around each line.
|
|
155
|
-
|
|
161
|
+
|
|
156
162
|
In the GeoDataFrame using its 'max_width' attribute and
|
|
157
163
|
saves the resulting polygons in a new shapefile.
|
|
158
164
|
|
|
@@ -177,67 +183,50 @@ def generate_fixed_width_footprint(line_gdf, max_width=False):
|
|
|
177
183
|
if not max_width:
|
|
178
184
|
print("Using quantile 75% width")
|
|
179
185
|
buffer_gdf["geometry"] = line_gdf.apply(
|
|
180
|
-
lambda row: row.geometry.buffer(row.avg_width / 2)
|
|
181
|
-
if row.geometry is not None
|
|
182
|
-
else None,
|
|
186
|
+
lambda row: row.geometry.buffer(row.avg_width / 2) if row.geometry is not None else None,
|
|
183
187
|
axis=1,
|
|
184
188
|
)
|
|
185
189
|
else:
|
|
186
190
|
print("Using quantile 90% + 20% width")
|
|
187
191
|
buffer_gdf["geometry"] = line_gdf.apply(
|
|
188
|
-
lambda row: row.geometry.buffer(row.max_width * 1.2 / 2)
|
|
189
|
-
if row.geometry is not None
|
|
190
|
-
else None,
|
|
192
|
+
lambda row: row.geometry.buffer(row.max_width * 1.2 / 2) if row.geometry is not None else None,
|
|
191
193
|
axis=1,
|
|
192
194
|
)
|
|
193
195
|
|
|
194
196
|
return buffer_gdf
|
|
195
197
|
|
|
196
198
|
|
|
197
|
-
def smooth_linestring(line, tolerance=0.5):
|
|
198
|
-
"""
|
|
199
|
-
Smooths a LineString geometry using the Ramer-Douglas-Peucker algorithm.
|
|
200
|
-
|
|
201
|
-
Args:
|
|
202
|
-
line: The LineString geometry to smooth.
|
|
203
|
-
tolerance: The maximum distance from a point to a line for the point
|
|
204
|
-
to be considered part of the line.
|
|
205
|
-
|
|
206
|
-
Returns:
|
|
207
|
-
The smoothed LineString geometry.
|
|
208
|
-
|
|
209
|
-
"""
|
|
210
|
-
simplified_line = line.simplify(tolerance)
|
|
211
|
-
# simplified_line = line
|
|
212
|
-
return simplified_line
|
|
213
|
-
|
|
214
|
-
|
|
215
199
|
def calculate_average_width(line, in_poly, offset, n_samples):
|
|
216
200
|
"""Calculate the average width of a polygon perpendicular to the given line."""
|
|
217
201
|
# Smooth the line
|
|
218
202
|
try:
|
|
219
|
-
line =
|
|
203
|
+
line = line.simplify(0.1)
|
|
220
204
|
|
|
221
205
|
valid_widths = 0
|
|
222
206
|
sample_points = generate_sample_points(line, n_samples=n_samples)
|
|
223
|
-
sample_points_pairs = list(
|
|
224
|
-
zip(sample_points[:-2], sample_points[1:-1], sample_points[2:])
|
|
225
|
-
)
|
|
207
|
+
sample_points_pairs = list(zip(sample_points[:-2], sample_points[1:-1], sample_points[2:]))
|
|
226
208
|
widths = np.zeros(len(sample_points_pairs))
|
|
227
209
|
perp_lines = []
|
|
228
210
|
perp_lines_original = []
|
|
229
211
|
except Exception as e:
|
|
230
212
|
print(e)
|
|
231
|
-
|
|
213
|
+
|
|
232
214
|
try:
|
|
233
215
|
for i, points in enumerate(sample_points_pairs):
|
|
234
|
-
|
|
235
|
-
points, offset=offset
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
216
|
+
try:
|
|
217
|
+
perp_line = algo_common.generate_perpendicular_line_precise(points, offset=offset)
|
|
218
|
+
perp_lines_original.append(perp_line)
|
|
219
|
+
except Exception as e:
|
|
220
|
+
print(f"Failed to generate perpendicular at index {i}: {e}")
|
|
221
|
+
perp_lines_original.append(None)
|
|
222
|
+
continue
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
polygon_intersect = in_poly.iloc[in_poly.sindex.query(perp_line)]
|
|
226
|
+
intersections = polygon_intersect.intersection(perp_line)
|
|
227
|
+
except Exception as e:
|
|
228
|
+
print(f"Failed intersection at index {i}: {e}")
|
|
229
|
+
intersections = []
|
|
241
230
|
|
|
242
231
|
line_list = []
|
|
243
232
|
for inter in intersections:
|
|
@@ -262,6 +251,10 @@ def calculate_average_width(line, in_poly, offset, n_samples):
|
|
|
262
251
|
widths[i] = max(widths[i], item.length)
|
|
263
252
|
valid_widths += 1
|
|
264
253
|
|
|
254
|
+
# Todo: check missing perpendicular lines
|
|
255
|
+
# if len(perp_lines_original) < len(sample_points_pairs):
|
|
256
|
+
# print(f"Missing perpendicular at index {i}")
|
|
257
|
+
|
|
265
258
|
except Exception as e:
|
|
266
259
|
print(f"loop: {e}")
|
|
267
260
|
|
|
@@ -273,48 +266,57 @@ def calculate_average_width(line, in_poly, offset, n_samples):
|
|
|
273
266
|
)
|
|
274
267
|
|
|
275
268
|
|
|
276
|
-
def
|
|
269
|
+
def ground_footprint(
|
|
277
270
|
in_line,
|
|
278
271
|
in_footprint,
|
|
279
272
|
n_samples,
|
|
280
273
|
offset,
|
|
281
274
|
max_width,
|
|
282
275
|
out_footprint,
|
|
283
|
-
processes,
|
|
284
|
-
verbose,
|
|
285
|
-
in_layer=None,
|
|
286
276
|
in_layer_lc_path="least_cost_path",
|
|
287
|
-
in_layer_fp=None,
|
|
288
|
-
out_layer=None,
|
|
289
277
|
merge_group=True,
|
|
290
278
|
width_percentile=75,
|
|
291
|
-
|
|
279
|
+
trim_output=True,
|
|
280
|
+
processes=0, call_mode=CallMode.CLI, log_level="INFO"
|
|
292
281
|
):
|
|
282
|
+
in_file, in_layer = sp_common.decode_file_layer(in_line)
|
|
283
|
+
in_fp_file, in_layer_fp = sp_common.decode_file_layer(in_footprint)
|
|
284
|
+
out_file, out_layer = sp_common.decode_file_layer(out_footprint)
|
|
285
|
+
|
|
293
286
|
n_samples = int(n_samples)
|
|
294
287
|
offset = float(offset)
|
|
295
|
-
width_percentile=int(width_percentile)
|
|
288
|
+
width_percentile = int(width_percentile)
|
|
296
289
|
|
|
297
290
|
# TODO: refactor this code for better line quality check
|
|
298
|
-
line_gdf = gpd.read_file(
|
|
299
|
-
|
|
291
|
+
line_gdf = gpd.read_file(in_file, layer=in_layer)
|
|
292
|
+
if bt_const.BT_GROUP not in line_gdf.columns:
|
|
293
|
+
line_gdf[bt_const.BT_GROUP] = range(1, len(line_gdf) + 1)
|
|
294
|
+
|
|
295
|
+
use_least_cost_path = True
|
|
296
|
+
try:
|
|
297
|
+
lc_path_gdf = gpd.read_file(in_file, layer=in_layer_lc_path)
|
|
298
|
+
except (ValueError, OSError, pyogrio.errors.DataLayerError):
|
|
299
|
+
print(f"Layer '{in_layer_lc_path}' not found in {in_file}, skipping least cost path logic.")
|
|
300
|
+
use_least_cost_path = False
|
|
301
|
+
|
|
300
302
|
if not merge_group:
|
|
301
|
-
line_gdf
|
|
302
|
-
|
|
303
|
-
|
|
303
|
+
line_gdf["geometry"] = line_gdf.geometry.apply(custom_line_merge)
|
|
304
|
+
if use_least_cost_path:
|
|
305
|
+
lc_path_gdf["geometry"] = lc_path_gdf.geometry.apply(custom_line_merge)
|
|
306
|
+
|
|
304
307
|
line_gdf = algo_common.clean_line_geometries(line_gdf)
|
|
305
308
|
|
|
306
309
|
# read footprints and remove holes
|
|
307
|
-
poly_gdf = gpd.read_file(
|
|
310
|
+
poly_gdf = gpd.read_file(in_fp_file, layer=in_layer_fp)
|
|
308
311
|
poly_gdf["geometry"] = poly_gdf["geometry"].apply(algo_common.remove_holes)
|
|
309
|
-
|
|
310
|
-
# split lines
|
|
312
|
+
|
|
313
|
+
# merge group and/or split lines at intersections
|
|
311
314
|
merged_line_gdf = line_gdf.copy(deep=True)
|
|
312
315
|
if merge_group:
|
|
313
316
|
lg = LineGrouping(line_gdf, merge_group)
|
|
314
317
|
lg.run_grouping()
|
|
315
318
|
merged_line_gdf = lg.run_line_merge()
|
|
316
319
|
else:
|
|
317
|
-
# merge group first, then not merge after splitting
|
|
318
320
|
try:
|
|
319
321
|
lg = LineGrouping(line_gdf, not merge_group)
|
|
320
322
|
lg.run_grouping()
|
|
@@ -329,78 +331,99 @@ def line_footprint_fixed(
|
|
|
329
331
|
)
|
|
330
332
|
|
|
331
333
|
# least cost path merge and split
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
334
|
+
if use_least_cost_path:
|
|
335
|
+
lg_leastcost = LineGrouping(lc_path_gdf, not merge_group)
|
|
336
|
+
lg_leastcost.run_grouping()
|
|
337
|
+
merged_lc_path_gdf = lg_leastcost.run_line_merge()
|
|
338
|
+
splitter_leastcost = LineSplitter(merged_lc_path_gdf)
|
|
339
|
+
splitter_leastcost.process(splitter.intersection_gdf)
|
|
340
|
+
|
|
341
|
+
splitter_leastcost.save_to_geopackage(
|
|
342
|
+
out_footprint,
|
|
343
|
+
line_layer="split_leastcost",
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
lg = LineGrouping(splitter.split_lines_gdf, merge_group)
|
|
347
|
+
lg.run_grouping()
|
|
348
|
+
merged_line_gdf = lg.run_line_merge()
|
|
346
349
|
except ValueError as e:
|
|
347
|
-
print(f"Exception:
|
|
350
|
+
print(f"Exception: ground_footprint: {e}")
|
|
348
351
|
|
|
349
352
|
# save original merged lines
|
|
350
|
-
merged_line_gdf.to_file(
|
|
353
|
+
merged_line_gdf.to_file(out_file, layer="merged_lines_original")
|
|
351
354
|
|
|
352
355
|
# prepare line arguments
|
|
353
|
-
line_args = prepare_line_args(
|
|
354
|
-
merged_line_gdf, poly_gdf, n_samples, offset, width_percentile
|
|
355
|
-
)
|
|
356
|
+
line_args = prepare_line_args(merged_line_gdf, poly_gdf, n_samples, offset, width_percentile)
|
|
356
357
|
|
|
357
358
|
out_lines = execute_multiprocessing(
|
|
358
|
-
process_single_line, line_args, "Fixed footprint", processes,
|
|
359
|
+
process_single_line, line_args, "Fixed footprint", processes, call_mode
|
|
359
360
|
)
|
|
360
361
|
line_attr = pd.concat(out_lines)
|
|
361
362
|
|
|
362
|
-
|
|
363
|
+
# Ensure BT_GROUP is present in line_attr
|
|
364
|
+
if bt_const.BT_GROUP not in line_attr.columns:
|
|
365
|
+
raise ValueError("BT_GROUP column is required in line_attr but is missing.")
|
|
366
|
+
|
|
363
367
|
# update avg_width and max_width by max value of group
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
+
if not merge_group:
|
|
369
|
+
group_max = (
|
|
370
|
+
line_attr.groupby(bt_const.BT_GROUP).agg({"avg_width": "max", "max_width": "max"}).reset_index()
|
|
371
|
+
)
|
|
368
372
|
|
|
369
|
-
|
|
370
|
-
|
|
373
|
+
# Merge the result back to the original dataframe based on 'group'
|
|
374
|
+
line_attr = line_attr.merge(group_max, on=bt_const.BT_GROUP, suffixes=("", "_max"))
|
|
371
375
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
376
|
+
# Overwrite the original columns directly with the max values
|
|
377
|
+
line_attr["avg_width"] = line_attr["avg_width_max"]
|
|
378
|
+
line_attr["max_width"] = line_attr["max_width_max"]
|
|
375
379
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
# Done: updating avg_width and max_width
|
|
379
|
-
############################################
|
|
380
|
+
# Drop the temporary max columns
|
|
381
|
+
line_attr.drop(columns=["avg_width_max", "max_width_max"], inplace=True)
|
|
380
382
|
|
|
381
|
-
# create fixed width footprint
|
|
383
|
+
# create fixed width footprint (always assign buffer_gdf)
|
|
384
|
+
print("Step: Generating fixed width footprints")
|
|
382
385
|
buffer_gdf = generate_fixed_width_footprint(line_attr, max_width=max_width)
|
|
383
386
|
|
|
384
|
-
#
|
|
387
|
+
# reserve all layers for output
|
|
385
388
|
perp_lines_gdf = buffer_gdf.copy(deep=True)
|
|
386
389
|
perp_lines_original_gdf = buffer_gdf.copy(deep=True)
|
|
387
390
|
|
|
388
|
-
#
|
|
391
|
+
# Save untrimmed fixed width footprint
|
|
389
392
|
buffer_gdf = buffer_gdf.drop(columns=["perp_lines"])
|
|
390
393
|
buffer_gdf = buffer_gdf.drop(columns=["perp_lines_original"])
|
|
391
394
|
buffer_gdf = buffer_gdf.set_crs(perp_lines_gdf.crs, allow_override=True)
|
|
392
395
|
buffer_gdf.reset_index(inplace=True, drop=True)
|
|
393
396
|
|
|
397
|
+
untrimmed_footprint = "untrimmed_footprint"
|
|
398
|
+
buffer_gdf.to_file(out_file, layer=untrimmed_footprint)
|
|
399
|
+
print(f"Untrimmed fixed width footprint saved as '{untrimmed_footprint}'")
|
|
400
|
+
|
|
394
401
|
# trim lines and footprints
|
|
395
|
-
|
|
396
|
-
|
|
402
|
+
if trim_output:
|
|
403
|
+
lg.run_cleanup(buffer_gdf)
|
|
404
|
+
|
|
405
|
+
# Ensure only polygons are saved in clean_footprint
|
|
406
|
+
def ensure_polygons(gdf, buffer_width=0.01):
|
|
407
|
+
gdf["geometry"] = gdf["geometry"].apply(
|
|
408
|
+
lambda geom: geom.buffer(buffer_width)
|
|
409
|
+
if geom.geom_type in ["LineString", "MultiLineString"]
|
|
410
|
+
else geom
|
|
411
|
+
)
|
|
412
|
+
gdf = gdf[gdf.geometry.type.isin(["Polygon", "MultiPolygon"])]
|
|
413
|
+
return gdf
|
|
414
|
+
|
|
415
|
+
# Patch: after trimming, ensure polygons in clean_footprint layer
|
|
416
|
+
if hasattr(lg, "merged_lines_trimmed") and lg.merged_lines_trimmed is not None:
|
|
417
|
+
lg.merged_lines_trimmed = ensure_polygons(lg.merged_lines_trimmed)
|
|
418
|
+
print("Step: Saving trimmed outputs")
|
|
419
|
+
lg.save_file(out_file, out_layer)
|
|
420
|
+
else:
|
|
421
|
+
print("Skipping line and footprint trimming per user option.")
|
|
397
422
|
|
|
398
423
|
# perpendicular lines
|
|
399
424
|
layer = "perp_lines"
|
|
400
|
-
|
|
401
|
-
out_aux_gpkg =
|
|
402
|
-
".gpkg"
|
|
403
|
-
)
|
|
425
|
+
out_footprint_path = Path(out_file)
|
|
426
|
+
out_aux_gpkg = out_footprint_path.with_stem(out_footprint_path.stem + "_aux").with_suffix(".gpkg")
|
|
404
427
|
perp_lines_gdf = perp_lines_gdf.set_geometry("perp_lines")
|
|
405
428
|
perp_lines_gdf = perp_lines_gdf.drop(columns=["perp_lines_original"])
|
|
406
429
|
perp_lines_gdf = perp_lines_gdf.drop(columns=["geometry"])
|
|
@@ -408,29 +431,29 @@ def line_footprint_fixed(
|
|
|
408
431
|
perp_lines_gdf.to_file(out_aux_gpkg.as_posix(), layer=layer)
|
|
409
432
|
|
|
410
433
|
layer = "perp_lines_original"
|
|
411
|
-
perp_lines_original_gdf = perp_lines_original_gdf.set_geometry(
|
|
412
|
-
"perp_lines_original"
|
|
413
|
-
)
|
|
434
|
+
perp_lines_original_gdf = perp_lines_original_gdf.set_geometry("perp_lines_original")
|
|
414
435
|
perp_lines_original_gdf = perp_lines_original_gdf.drop(columns=["perp_lines"])
|
|
415
436
|
perp_lines_original_gdf = perp_lines_original_gdf.drop(columns=["geometry"])
|
|
416
|
-
perp_lines_original_gdf = perp_lines_original_gdf.set_crs(
|
|
417
|
-
buffer_gdf.crs, allow_override=True
|
|
418
|
-
)
|
|
437
|
+
perp_lines_original_gdf = perp_lines_original_gdf.set_crs(buffer_gdf.crs, allow_override=True)
|
|
419
438
|
perp_lines_original_gdf.to_file(out_aux_gpkg.as_posix(), layer=layer)
|
|
420
439
|
|
|
421
440
|
layer = "centerline_simplified"
|
|
441
|
+
# Drop perp_lines_original column if present to avoid export warnings
|
|
442
|
+
if "perp_lines_original" in line_attr.columns:
|
|
443
|
+
line_attr = line_attr.drop(columns=["perp_lines_original"])
|
|
422
444
|
line_attr = line_attr.drop(columns="perp_lines")
|
|
423
445
|
line_attr.to_file(out_aux_gpkg.as_posix(), layer=layer)
|
|
424
446
|
|
|
425
447
|
# save footprints without holes
|
|
426
448
|
poly_gdf.to_file(out_aux_gpkg.as_posix(), layer="footprint_no_holes")
|
|
449
|
+
print("Step: Finished fixed width footprint tool")
|
|
427
450
|
|
|
428
|
-
print("Fixed width footprint tool finished.")
|
|
429
451
|
|
|
430
452
|
if __name__ == "__main__":
|
|
431
|
-
|
|
453
|
+
import time
|
|
454
|
+
|
|
455
|
+
from beratools.utility.tool_args import compose_tool_kwargs
|
|
432
456
|
start_time = time.time()
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
)
|
|
457
|
+
kwargs = compose_tool_kwargs("ground_footprint")
|
|
458
|
+
ground_footprint(**kwargs)
|
|
436
459
|
print("Elapsed time: {}".format(time.time() - start_time))
|
|
@@ -29,45 +29,74 @@
|
|
|
29
29
|
# cost corridor method and individual line thresholds.
|
|
30
30
|
#
|
|
31
31
|
# ---------------------------------------------------------------------------
|
|
32
|
-
import sys
|
|
33
|
-
from pathlib import Path
|
|
34
|
-
from inspect import getsourcefile
|
|
35
32
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
from beratools.core.line_footprint_functions import *
|
|
34
|
+
from beratools.core.canopy_threshold_relative import *
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def line_footprint_relative(
|
|
38
|
+
in_line,
|
|
39
|
+
in_chm,
|
|
40
|
+
max_ln_width,
|
|
41
|
+
exp_shk_cell,
|
|
42
|
+
out_footprint,
|
|
43
|
+
out_centerline,
|
|
44
|
+
off_ln_dist,
|
|
45
|
+
canopy_percentile,
|
|
46
|
+
canopy_thresh_percentage,
|
|
47
|
+
tree_radius,
|
|
48
|
+
max_line_dist,
|
|
49
|
+
canopy_avoidance,
|
|
50
|
+
exponent,
|
|
51
|
+
processes,
|
|
52
|
+
call_mode,
|
|
53
|
+
log_level,
|
|
54
|
+
):
|
|
55
|
+
|
|
56
|
+
verbose = True if call_mode == CallMode.GUI.value else False
|
|
40
57
|
|
|
41
|
-
|
|
42
|
-
|
|
58
|
+
dy_cl_line = main_canopy_threshold_relative(
|
|
59
|
+
in_line=in_line,
|
|
60
|
+
in_chm=in_chm,
|
|
61
|
+
canopy_percentile=int(float(canopy_percentile)),
|
|
62
|
+
canopy_thresh_percentage=int(float(canopy_thresh_percentage)),
|
|
63
|
+
full_step=bool(True),
|
|
64
|
+
processes=int(float(processes)),
|
|
65
|
+
verbose=bool(verbose),
|
|
66
|
+
)
|
|
43
67
|
|
|
44
|
-
if
|
|
68
|
+
if not dy_cl_line:
|
|
69
|
+
print("[error]: main_canopy_threshold_relative did not return a valid path. Aborting footprint step.")
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
main_line_footprint_relative(
|
|
73
|
+
in_line=dy_cl_line,
|
|
74
|
+
in_chm=in_chm,
|
|
75
|
+
max_ln_width=float(max_ln_width),
|
|
76
|
+
out_footprint=out_footprint or "",
|
|
77
|
+
out_centerline=out_centerline or "",
|
|
78
|
+
exp_shk_cell=int(float(exp_shk_cell)),
|
|
79
|
+
tree_radius=float(tree_radius),
|
|
80
|
+
max_line_dist=float(max_line_dist),
|
|
81
|
+
canopy_avoidance=float(canopy_avoidance),
|
|
82
|
+
exponent=float(exponent),
|
|
83
|
+
full_step=bool(True),
|
|
84
|
+
canopy_thresh_percentage=int(float(canopy_thresh_percentage)),
|
|
85
|
+
processes=int(float(processes)),
|
|
86
|
+
verbose=bool(verbose),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
from beratools.utility.tool_args import CallMode, compose_tool_kwargs
|
|
45
91
|
start_time = time.time()
|
|
46
|
-
print(
|
|
47
|
-
print(
|
|
92
|
+
print("[info]: Dynamic CC and Footprint processing started")
|
|
93
|
+
print("[info]: Current time: {}".format(time.strftime("%d %b %Y %H:%M:%S", time.localtime())))
|
|
48
94
|
|
|
49
|
-
|
|
50
|
-
parser.add_argument('-i', '--input', type=json.loads)
|
|
51
|
-
parser.add_argument('-p', '--processes')
|
|
52
|
-
parser.add_argument('-v', '--verbose')
|
|
53
|
-
args = parser.parse_args()
|
|
54
|
-
args.input['full_step'] = True
|
|
55
|
-
del args.input['out_footprint']
|
|
56
|
-
del args.input['out_centerline']
|
|
57
|
-
del args.input['exp_shk_cell']
|
|
58
|
-
del args.input['max_ln_width']
|
|
95
|
+
args=compose_tool_kwargs("line_footprint_relative")
|
|
59
96
|
|
|
60
|
-
|
|
61
|
-
dy_cl_line = main_canopy_threshold_relative(print, **args.input, processes=int(args.processes), verbose=verbose)
|
|
62
|
-
args = parser.parse_args()
|
|
63
|
-
args.input['full_step'] = True
|
|
64
|
-
args.input["in_line"] = dy_cl_line
|
|
65
|
-
del args.input['off_ln_dist']
|
|
66
|
-
del args.input['canopy_percentile']
|
|
67
|
-
verbose = True if args.verbose == 'True' else False
|
|
68
|
-
main_line_footprint_relative(print, **args.input, processes=int(args.processes), verbose=verbose)
|
|
97
|
+
line_footprint_relative(**args)
|
|
69
98
|
|
|
70
|
-
print(
|
|
71
|
-
print(
|
|
72
|
-
print(
|
|
73
|
-
print(
|
|
99
|
+
print("{}%".format(100))
|
|
100
|
+
print("[info]: Dynamic CC and Footprint processes finished")
|
|
101
|
+
print("[info]: Current time: {}".format(time.strftime("%d %b %Y %H:%M:%S", time.localtime())))
|
|
102
|
+
print("[info]: Total processing time (seconds): {}".format(round(time.time() - start_time, 3)))
|
beratools/tools/tool_template.py
CHANGED
|
@@ -7,63 +7,71 @@ See <https://gnu.org/licenses/gpl-3.0> for full license details.
|
|
|
7
7
|
Author: AUTHOR NAME
|
|
8
8
|
|
|
9
9
|
Description:
|
|
10
|
-
This script is
|
|
10
|
+
This script is tool template for the BERA Tools. The tool showcases
|
|
11
|
+
how to create a new tool for the BERA Tools framework. It is a
|
|
12
|
+
starting point for developers to implement their own tools.
|
|
13
|
+
It uses the GeoPandas to read and write geospatial data,
|
|
14
|
+
and the multiprocessing library to process the geospatial data.
|
|
15
|
+
|
|
16
|
+
To integrate with GUI, work in the gui/assets/beratools.json is needed.
|
|
17
|
+
Please see developer's guide for more details.
|
|
18
|
+
|
|
11
19
|
Webpage: https://github.com/appliedgrg/beratools
|
|
12
20
|
|
|
13
21
|
The purpose of this script is to provide template for tool.
|
|
14
22
|
"""
|
|
15
|
-
import time
|
|
16
|
-
from multiprocessing.pool import Pool
|
|
17
|
-
from random import random
|
|
18
23
|
|
|
19
|
-
import
|
|
24
|
+
import geopandas as gpd
|
|
25
|
+
import pandas as pd
|
|
20
26
|
|
|
21
|
-
import beratools.
|
|
27
|
+
import beratools.utility.spatial_common as sp_common
|
|
22
28
|
from beratools.core.tool_base import execute_multiprocessing
|
|
29
|
+
from beratools.utility.tool_args import CallMode
|
|
23
30
|
|
|
24
|
-
# Example task_data as a list of numpy ndarrays
|
|
25
|
-
task_data_list = [
|
|
26
|
-
np.array([1, 2, 3]),
|
|
27
|
-
np.array([4, 5, 6]),
|
|
28
|
-
np.array([7, 8, 9])
|
|
29
|
-
]
|
|
30
31
|
|
|
31
|
-
def
|
|
32
|
-
|
|
33
|
-
):
|
|
32
|
+
def tool_template(in_feature, buffer_dist, out_feature,
|
|
33
|
+
processes=0, call_mode=CallMode.CLI, log_level="INFO"):
|
|
34
34
|
"""
|
|
35
35
|
Define tool entry point.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
in_feature: input feature (encoded as "file:layer")
|
|
39
|
+
buffer_dist: buffer for input lines
|
|
40
|
+
out_feature: output feature (encoded as "file:layer")
|
|
41
|
+
processes: number of processes to use
|
|
42
|
+
verbose: verbosity level
|
|
43
|
+
|
|
44
|
+
These arguments are defined in beratools.json file. Whenever possible,
|
|
45
|
+
use execute_multiprocessing to run tasks in parallel to speedup.
|
|
46
|
+
|
|
39
47
|
"""
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
task_data_list,
|
|
43
|
-
"tool_template",
|
|
44
|
-
processes,
|
|
45
|
-
verbose=verbose
|
|
46
|
-
)
|
|
47
|
-
print(len(result))
|
|
48
|
+
in_file, in_layer = sp_common.decode_file_layer(in_feature)
|
|
49
|
+
out_file, out_layer = sp_common.decode_file_layer(out_feature)
|
|
48
50
|
|
|
51
|
+
buffer_dist = float(buffer_dist)
|
|
52
|
+
gdf = gpd.read_file(in_file, layer=in_layer)
|
|
53
|
+
gdf_list = [(gdf.iloc[[i]], buffer_dist) for i in range(len(gdf))]
|
|
49
54
|
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
# Set verbose based on log_level
|
|
56
|
+
results = execute_multiprocessing(buffer_worker, gdf_list, "tool_template", processes, call_mode)
|
|
57
|
+
|
|
58
|
+
buffered_gdf = gpd.GeoDataFrame(pd.concat(results, ignore_index=True), crs=gdf.crs)
|
|
59
|
+
buffered_gdf.to_file(out_file, layer=out_layer)
|
|
55
60
|
|
|
56
|
-
# block for a moment
|
|
57
|
-
time.sleep(value * 10)
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
# task executed in a worker process
|
|
63
|
+
def buffer_worker(in_args):
|
|
64
|
+
buffered = in_args[0].copy()
|
|
65
|
+
buffer_dist = in_args[1]
|
|
66
|
+
buffered["geometry"] = buffered.geometry.buffer(buffer_dist)
|
|
67
|
+
return buffered
|
|
68
|
+
|
|
61
69
|
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
import time
|
|
62
72
|
|
|
63
|
-
|
|
64
|
-
in_args, in_verbose = bt_common.check_arguments()
|
|
73
|
+
from beratools.utility.tool_args import compose_tool_kwargs
|
|
65
74
|
start_time = time.time()
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
)
|
|
75
|
+
kwargs = compose_tool_kwargs("tool_template")
|
|
76
|
+
tool_template(**kwargs)
|
|
69
77
|
print("Elapsed time: {}".format(time.time() - start_time))
|