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
beratools/__init__.py
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
-
__author__ = """AppliedGRG"""
|
|
2
|
-
__email__ = 'appliedgrg@gmail.com'
|
|
3
|
-
__version__ = '0.
|
|
1
|
+
__author__ = """AppliedGRG"""
|
|
2
|
+
__email__ = 'appliedgrg@gmail.com'
|
|
3
|
+
__version__ = '0.1.0'
|
|
4
|
+
__license__ = "GPL-3.0-or-later"
|
|
5
|
+
__copyright__ = "Copyright (c) AppliedGRG"
|
|
6
|
+
__status__ = "Pre-Alpha"
|
|
7
|
+
__url__ = "https://github.com/appliedgrg/beratools"
|
|
8
|
+
__all__ = ["tools", "gui"]
|
|
@@ -10,12 +10,12 @@ Description:
|
|
|
10
10
|
This script is part of the BERA Tools.
|
|
11
11
|
Webpage: https://github.com/appliedgrg/beratools
|
|
12
12
|
|
|
13
|
-
The purpose of this script is to provide main interface for canopy footprint tool.
|
|
14
|
-
The tool is used to generate the footprint of a line based on relative threshold.
|
|
13
|
+
The purpose of this script is to provide main interface for experimental canopy footprint tool.
|
|
14
|
+
The tool is used to generate the canopy footprint of a line based on relative threshold.
|
|
15
15
|
"""
|
|
16
|
+
|
|
16
17
|
import math
|
|
17
|
-
import
|
|
18
|
-
from enum import StrEnum
|
|
18
|
+
from enum import Enum
|
|
19
19
|
|
|
20
20
|
import geopandas as gpd
|
|
21
21
|
import numpy as np
|
|
@@ -30,47 +30,79 @@ import beratools.core.algo_common as algo_common
|
|
|
30
30
|
import beratools.core.algo_cost as algo_cost
|
|
31
31
|
import beratools.core.constants as bt_const
|
|
32
32
|
import beratools.core.tool_base as bt_base
|
|
33
|
-
import beratools.
|
|
33
|
+
import beratools.utility.spatial_common as sp_common
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
class Side(
|
|
36
|
+
class Side(Enum):
|
|
37
37
|
"""Constants for left and right side."""
|
|
38
38
|
|
|
39
39
|
left = "left"
|
|
40
40
|
right = "right"
|
|
41
41
|
|
|
42
|
+
|
|
42
43
|
class FootprintCanopy:
|
|
43
44
|
"""Relative canopy footprint class."""
|
|
44
45
|
|
|
45
|
-
def __init__(self, in_geom, in_chm
|
|
46
|
-
|
|
46
|
+
def __init__(self, in_geom, in_chm):
|
|
47
|
+
in_file, in_layer = sp_common.decode_file_layer(in_geom)
|
|
48
|
+
data = gpd.read_file(in_file, layer=in_layer)
|
|
47
49
|
self.lines = []
|
|
48
50
|
|
|
49
51
|
for idx in data.index:
|
|
50
52
|
line = LineInfo(data.iloc[[idx]], in_chm)
|
|
51
53
|
self.lines.append(line)
|
|
52
54
|
|
|
53
|
-
def compute(self, processes
|
|
55
|
+
def compute(self, processes):
|
|
54
56
|
result = bt_base.execute_multiprocessing(
|
|
55
57
|
algo_common.process_single_item,
|
|
56
58
|
self.lines,
|
|
57
59
|
"Canopy Footprint",
|
|
58
60
|
processes,
|
|
59
|
-
1,
|
|
60
|
-
parallel_mode,
|
|
61
61
|
)
|
|
62
62
|
|
|
63
|
-
fp = [
|
|
64
|
-
|
|
63
|
+
fp = []
|
|
64
|
+
percentile = []
|
|
65
|
+
try:
|
|
66
|
+
for item in result:
|
|
67
|
+
if item.footprint is not None:
|
|
68
|
+
fp.append(item.footprint)
|
|
69
|
+
else:
|
|
70
|
+
print("Footprint is None for one of the lines.")
|
|
71
|
+
continue # Skip failed line
|
|
72
|
+
|
|
73
|
+
if fp:
|
|
74
|
+
self.footprints = pd.concat(fp)
|
|
75
|
+
else:
|
|
76
|
+
print("No valid footprints to save.")
|
|
77
|
+
self.footprints = None
|
|
78
|
+
|
|
79
|
+
for item in result:
|
|
80
|
+
if item.lines_percentile is not None:
|
|
81
|
+
percentile.append(item.lines_percentile)
|
|
82
|
+
else:
|
|
83
|
+
print("lines_percentile is None for one of the lines.")
|
|
84
|
+
continue # Skip failed line
|
|
65
85
|
|
|
66
|
-
|
|
67
|
-
|
|
86
|
+
if percentile:
|
|
87
|
+
self.lines_percentile = pd.concat(percentile)
|
|
88
|
+
else:
|
|
89
|
+
print("No valid lines_percentile to save.")
|
|
90
|
+
self.lines_percentile = None
|
|
91
|
+
except Exception as e:
|
|
92
|
+
print(f"Error during processing: {e}")
|
|
68
93
|
|
|
69
94
|
def save_footprint(self, out_footprint, layer=None):
|
|
70
|
-
self.footprints.
|
|
95
|
+
if self.footprints is not None and isinstance(self.footprints, gpd.GeoDataFrame):
|
|
96
|
+
self.footprints.to_file(out_footprint, layer=layer)
|
|
97
|
+
else:
|
|
98
|
+
print("No footprints to save (None or not a GeoDataFrame).")
|
|
71
99
|
|
|
72
100
|
def save_line_percentile(self, out_percentile):
|
|
73
|
-
self.lines_percentile.
|
|
101
|
+
if self.lines_percentile is not None and isinstance(self.lines_percentile, gpd.GeoDataFrame):
|
|
102
|
+
self.lines_percentile.to_file(out_percentile)
|
|
103
|
+
else:
|
|
104
|
+
print("No lines_percentile to save (None or not a GeoDataFrame).")
|
|
105
|
+
|
|
74
106
|
|
|
75
107
|
class BufferRing:
|
|
76
108
|
"""Buffer ring class."""
|
|
@@ -81,28 +113,27 @@ class BufferRing:
|
|
|
81
113
|
self.percentile = 0.5
|
|
82
114
|
self.Dyn_Canopy_Threshold = 0.05
|
|
83
115
|
|
|
116
|
+
|
|
84
117
|
class LineInfo:
|
|
85
118
|
"""Class to store line information."""
|
|
86
119
|
|
|
87
|
-
def __init__(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
120
|
+
def __init__(
|
|
121
|
+
self,
|
|
122
|
+
line_gdf,
|
|
123
|
+
in_chm,
|
|
124
|
+
max_ln_width=32,
|
|
125
|
+
tree_radius=1.5,
|
|
126
|
+
max_line_dist=1.5,
|
|
127
|
+
canopy_avoidance=0.0,
|
|
128
|
+
exponent=1.0,
|
|
129
|
+
canopy_thresh_percentage=50,
|
|
130
|
+
):
|
|
95
131
|
self.line = line_gdf
|
|
96
132
|
self.in_chm = in_chm
|
|
97
|
-
self.line_simp = self.line.geometry.simplify(
|
|
98
|
-
tolerance=0.5, preserve_topology=True
|
|
99
|
-
)
|
|
133
|
+
self.line_simp = self.line.geometry.simplify(tolerance=0.5, preserve_topology=True)
|
|
100
134
|
|
|
101
135
|
self.canopy_percentile = 50
|
|
102
136
|
self.DynCanTh = np.nan
|
|
103
|
-
# chk_df_multipart
|
|
104
|
-
# if proc_segments:
|
|
105
|
-
# line_seg = split_into_segments(line_seg)
|
|
106
137
|
|
|
107
138
|
self.buffer_rings = []
|
|
108
139
|
|
|
@@ -127,16 +158,38 @@ class LineInfo:
|
|
|
127
158
|
self.buffer_right = None
|
|
128
159
|
self.footprint = None
|
|
129
160
|
|
|
161
|
+
self.lines_percentile = None
|
|
162
|
+
|
|
130
163
|
def compute(self):
|
|
131
164
|
self.prepare_ring_buffer()
|
|
132
165
|
|
|
133
166
|
ring_list = []
|
|
134
167
|
for item in self.buffer_rings:
|
|
135
168
|
ring = self.cal_percentileRing(item)
|
|
136
|
-
|
|
169
|
+
if ring is not None:
|
|
170
|
+
ring_list.append(ring)
|
|
171
|
+
else:
|
|
172
|
+
# print("Skipping invalid ring.")
|
|
173
|
+
# TODO: handle None rings appropriately
|
|
174
|
+
pass
|
|
137
175
|
|
|
138
176
|
self.buffer_rings = ring_list
|
|
139
177
|
|
|
178
|
+
# Aggregate percentiles and geometries for lines_percentile
|
|
179
|
+
percentile_records = []
|
|
180
|
+
for ring in self.buffer_rings:
|
|
181
|
+
if ring is not None and hasattr(ring, "geometry") and hasattr(ring, "percentile"):
|
|
182
|
+
percentile_records.append(
|
|
183
|
+
{"geometry": ring.geometry, "percentile": ring.percentile, "side": ring.side.value}
|
|
184
|
+
)
|
|
185
|
+
if percentile_records:
|
|
186
|
+
self.lines_percentile = gpd.GeoDataFrame(percentile_records)
|
|
187
|
+
self.lines_percentile.set_geometry("geometry", inplace=True)
|
|
188
|
+
if self.line.crs:
|
|
189
|
+
self.lines_percentile = self.lines_percentile.set_crs(self.line.crs, allow_override=True)
|
|
190
|
+
else:
|
|
191
|
+
self.lines_percentile = None
|
|
192
|
+
|
|
140
193
|
self.rate_of_change(self.get_percentile_array(Side.left), Side.left)
|
|
141
194
|
self.rate_of_change(self.get_percentile_array(Side.right), Side.right)
|
|
142
195
|
|
|
@@ -152,14 +205,35 @@ class LineInfo:
|
|
|
152
205
|
|
|
153
206
|
fp_left = self.process_single_footprint(Side.left)
|
|
154
207
|
fp_right = self.process_single_footprint(Side.right)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
208
|
+
|
|
209
|
+
# Check if footprints are valid
|
|
210
|
+
if fp_left is None or fp_right is None:
|
|
211
|
+
print("One or both footprints are None in LineInfo.")
|
|
212
|
+
self.footprint = None
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
# Buffer cleanup for validity
|
|
217
|
+
fp_left.geometry = fp_left.geometry.buffer(0)
|
|
218
|
+
fp_right.geometry = fp_right.geometry.buffer(0)
|
|
219
|
+
|
|
220
|
+
fp_combined = pd.concat([fp_left, fp_right])
|
|
221
|
+
|
|
222
|
+
if fp_combined.empty or not isinstance(fp_combined, gpd.GeoDataFrame):
|
|
223
|
+
print("Combined footprint is invalid or empty.")
|
|
224
|
+
self.footprint = None
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
fp_combined = fp_combined.dissolve()
|
|
228
|
+
fp_combined.geometry = fp_combined.geometry.buffer(-0.005)
|
|
229
|
+
|
|
230
|
+
self.footprint = fp_combined
|
|
231
|
+
except Exception as e:
|
|
232
|
+
print(f"Error combining footprints: {e}")
|
|
233
|
+
self.footprint = None
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
# Transfer group value to footprint if present
|
|
163
237
|
if bt_const.BT_GROUP in self.line.columns:
|
|
164
238
|
self.footprint[bt_const.BT_GROUP] = self.line[bt_const.BT_GROUP].iloc[0]
|
|
165
239
|
|
|
@@ -183,27 +257,25 @@ class LineInfo:
|
|
|
183
257
|
print("Empty buffer ring")
|
|
184
258
|
|
|
185
259
|
def cal_percentileRing(self, ring):
|
|
260
|
+
line_buffer = None
|
|
186
261
|
try:
|
|
187
262
|
line_buffer = ring.geometry
|
|
188
263
|
if line_buffer.is_empty or shapely.is_missing(line_buffer):
|
|
189
264
|
return None
|
|
190
265
|
if line_buffer.has_z:
|
|
191
|
-
line_buffer = sh_ops.transform(
|
|
192
|
-
lambda x, y, z=None: (x, y), line_buffer
|
|
193
|
-
)
|
|
266
|
+
line_buffer = sh_ops.transform(lambda x, y, z=None: (x, y), line_buffer)
|
|
194
267
|
|
|
195
268
|
except Exception as e:
|
|
196
269
|
print(f"cal_percentileRing: {e}")
|
|
270
|
+
return None
|
|
197
271
|
|
|
198
272
|
# TODO: temporary workaround for exception causing not percentile defined
|
|
199
273
|
try:
|
|
200
|
-
clipped_raster, _ =
|
|
274
|
+
clipped_raster, _ = sp_common.clip_raster(self.in_chm, line_buffer, 0)
|
|
201
275
|
clipped_raster = np.squeeze(clipped_raster, axis=0)
|
|
202
276
|
|
|
203
277
|
# mask all -9999 (nodata) value cells
|
|
204
|
-
masked_raster = np.ma.masked_where(
|
|
205
|
-
clipped_raster == bt_const.BT_NODATA, clipped_raster
|
|
206
|
-
)
|
|
278
|
+
masked_raster = np.ma.masked_where(clipped_raster == bt_const.BT_NODATA, clipped_raster)
|
|
207
279
|
filled_raster = np.ma.filled(masked_raster, np.nan)
|
|
208
280
|
|
|
209
281
|
# Calculate the percentile
|
|
@@ -288,7 +360,7 @@ class LineInfo:
|
|
|
288
360
|
except IndexError:
|
|
289
361
|
pass
|
|
290
362
|
|
|
291
|
-
# if still no result found, lower to 10% (1.1),
|
|
363
|
+
# if still no result found, lower to 10% (1.1),
|
|
292
364
|
# if no result found then default is used
|
|
293
365
|
if not found:
|
|
294
366
|
if 0.5 >= median_percentile:
|
|
@@ -318,31 +390,25 @@ class LineInfo:
|
|
|
318
390
|
"""
|
|
319
391
|
Buffers an input DataFrames geometry nring (number of rings) times.
|
|
320
392
|
|
|
321
|
-
Compute with a distance between rings of ringdist and returns
|
|
393
|
+
Compute with a distance between rings of ringdist and returns
|
|
322
394
|
a list of non overlapping buffers
|
|
323
395
|
"""
|
|
324
396
|
rings = [] # A list to hold the individual buffers
|
|
325
397
|
line = df.geometry.iloc[0]
|
|
326
398
|
# For each ring (1, 2, 3, ..., nrings)
|
|
327
|
-
for ring in np.arange(0, ringdist, nrings):
|
|
399
|
+
for ring in np.arange(0, ringdist, nrings):
|
|
328
400
|
big_ring = line.buffer(
|
|
329
401
|
nrings + ring, single_sided=True, cap_style="flat"
|
|
330
402
|
) # Create one big buffer
|
|
331
|
-
small_ring = line.buffer(
|
|
332
|
-
|
|
333
|
-
) # Create one smaller one
|
|
334
|
-
the_ring = big_ring.difference(
|
|
335
|
-
small_ring
|
|
336
|
-
) # Difference the big with the small to create a ring
|
|
403
|
+
small_ring = line.buffer(ring, single_sided=True, cap_style="flat") # Create one smaller one
|
|
404
|
+
the_ring = big_ring.difference(small_ring) # Difference the big with the small to create a ring
|
|
337
405
|
if (
|
|
338
406
|
~shapely.is_empty(the_ring)
|
|
339
407
|
or ~shapely.is_missing(the_ring)
|
|
340
408
|
or not None
|
|
341
409
|
or ~the_ring.area == 0
|
|
342
410
|
):
|
|
343
|
-
if isinstance(the_ring, sh_geom.MultiPolygon) or isinstance(
|
|
344
|
-
the_ring, shapely.Polygon
|
|
345
|
-
):
|
|
411
|
+
if isinstance(the_ring, sh_geom.MultiPolygon) or isinstance(the_ring, shapely.Polygon):
|
|
346
412
|
rings.append(the_ring) # Append the ring to the rings list
|
|
347
413
|
else:
|
|
348
414
|
if isinstance(the_ring, shapely.GeometryCollection):
|
|
@@ -379,40 +445,32 @@ class LineInfo:
|
|
|
379
445
|
|
|
380
446
|
def dyn_canopy_cost_raster(self, side):
|
|
381
447
|
in_chm_raster = self.in_chm
|
|
382
|
-
# tree_radius = self.tree_radius
|
|
383
|
-
# max_line_dist = self.max_line_dist
|
|
384
|
-
# canopy_avoid = self.canopy_avoidance
|
|
385
|
-
# exponent = self.exponent
|
|
386
448
|
line_df = self.line
|
|
387
449
|
out_meta = self.out_meta
|
|
388
450
|
|
|
389
451
|
canopy_thresh_percentage = self.canopy_thresh_percentage / 100
|
|
390
452
|
|
|
453
|
+
Cut_Dist = None
|
|
454
|
+
line_buffer = None
|
|
391
455
|
if side == Side.left:
|
|
392
|
-
canopy_ht_threshold = line_df.CL_CutHt * canopy_thresh_percentage
|
|
456
|
+
canopy_ht_threshold = line_df.CL_CutHt.iloc[0] * canopy_thresh_percentage
|
|
393
457
|
Cut_Dist = self.LDist_Cut
|
|
394
458
|
line_buffer = self.buffer_left
|
|
395
459
|
elif side == Side.right:
|
|
396
|
-
canopy_ht_threshold = line_df.CR_CutHt * canopy_thresh_percentage
|
|
460
|
+
canopy_ht_threshold = line_df.CR_CutHt.iloc[0] * canopy_thresh_percentage
|
|
397
461
|
Cut_Dist = self.RDist_Cut
|
|
398
462
|
line_buffer = self.buffer_right
|
|
399
463
|
else:
|
|
400
464
|
canopy_ht_threshold = 0.5
|
|
465
|
+
Cut_Dist = 1.0
|
|
466
|
+
line_buffer = None
|
|
401
467
|
|
|
402
468
|
canopy_ht_threshold = float(canopy_ht_threshold)
|
|
403
469
|
if canopy_ht_threshold <= 0:
|
|
404
470
|
canopy_ht_threshold = 0.5
|
|
405
471
|
|
|
406
|
-
# get the round up integer number for tree search radius
|
|
407
|
-
# tree_radius = float(tree_radius)
|
|
408
|
-
# max_line_dist = float(max_line_dist)
|
|
409
|
-
# canopy_avoid = float(canopy_avoid)
|
|
410
|
-
# cost_raster_exponent = float(exponent)
|
|
411
|
-
|
|
412
472
|
try:
|
|
413
|
-
clipped_rasterC, out_meta =
|
|
414
|
-
in_chm_raster, line_buffer, 0
|
|
415
|
-
)
|
|
473
|
+
clipped_rasterC, out_meta = sp_common.clip_raster(in_chm_raster, line_buffer, 0)
|
|
416
474
|
negative_cost_clip, dyn_canopy_ndarray = algo_cost.cost_raster(
|
|
417
475
|
clipped_rasterC,
|
|
418
476
|
out_meta,
|
|
@@ -431,13 +489,21 @@ class LineInfo:
|
|
|
431
489
|
|
|
432
490
|
def process_single_footprint(self, side):
|
|
433
491
|
# this will change segment content, and parameters will be changed
|
|
434
|
-
|
|
492
|
+
result = self.dyn_canopy_cost_raster(side)
|
|
493
|
+
if result is None:
|
|
494
|
+
return None
|
|
495
|
+
in_canopy_r, in_cost_r, in_meta, Cut_Dist = result
|
|
496
|
+
|
|
497
|
+
if in_canopy_r is None or in_cost_r is None or in_meta is None or Cut_Dist is None:
|
|
498
|
+
return None
|
|
435
499
|
|
|
436
500
|
if np.isnan(in_canopy_r).all():
|
|
437
501
|
print("Canopy raster empty")
|
|
502
|
+
return None
|
|
438
503
|
|
|
439
504
|
if np.isnan(in_cost_r).all():
|
|
440
505
|
print("Cost raster empty")
|
|
506
|
+
return None
|
|
441
507
|
|
|
442
508
|
exp_shk_cell = self.exponent # TODO: duplicate vars
|
|
443
509
|
no_data = self.nodata
|
|
@@ -448,8 +514,13 @@ class LineInfo:
|
|
|
448
514
|
segment_list = []
|
|
449
515
|
|
|
450
516
|
feat = self.line.geometry.iloc[0]
|
|
451
|
-
|
|
452
|
-
|
|
517
|
+
if hasattr(feat, "geoms"):
|
|
518
|
+
for geom in feat.geoms:
|
|
519
|
+
for coord in geom.coords:
|
|
520
|
+
segment_list.append(coord)
|
|
521
|
+
else:
|
|
522
|
+
for coord in feat.coords:
|
|
523
|
+
segment_list.append(coord)
|
|
453
524
|
|
|
454
525
|
cell_size_x = in_transform[0]
|
|
455
526
|
cell_size_y = -in_transform[4]
|
|
@@ -465,9 +536,7 @@ class LineInfo:
|
|
|
465
536
|
# generate 1m interval points along line
|
|
466
537
|
distance_delta = 1
|
|
467
538
|
distances = np.arange(0, feat.length, distance_delta)
|
|
468
|
-
multipoint_along_line = [
|
|
469
|
-
feat.interpolate(distance) for distance in distances
|
|
470
|
-
]
|
|
539
|
+
multipoint_along_line = [feat.interpolate(distance) for distance in distances]
|
|
471
540
|
multipoint_along_line.append(sh_geom.Point(segment_list[-1]))
|
|
472
541
|
# Rasterize points along line
|
|
473
542
|
rasterized_points_Alongln = ras_feat.rasterize(
|
|
@@ -481,12 +550,8 @@ class LineInfo:
|
|
|
481
550
|
points_Alongln = np.transpose(np.nonzero(rasterized_points_Alongln))
|
|
482
551
|
|
|
483
552
|
# Find minimum cost paths through an N-d costs array.
|
|
484
|
-
mcp_flexible1 = MCP_Flexible(
|
|
485
|
-
|
|
486
|
-
)
|
|
487
|
-
flex_cost_alongLn, flex_back_alongLn = mcp_flexible1.find_costs(
|
|
488
|
-
starts=points_Alongln
|
|
489
|
-
)
|
|
553
|
+
mcp_flexible1 = MCP_Flexible(in_cost_r, sampling=(cell_size_x, cell_size_y), fully_connected=True)
|
|
554
|
+
flex_cost_alongLn, flex_back_alongLn = mcp_flexible1.find_costs(starts=points_Alongln)
|
|
490
555
|
|
|
491
556
|
# Generate corridor
|
|
492
557
|
corridor = flex_cost_alongLn
|
|
@@ -507,9 +572,7 @@ class LineInfo:
|
|
|
507
572
|
corridor_th_value = bt_const.FP_CORRIDOR_THRESHOLD / cell_size_x
|
|
508
573
|
|
|
509
574
|
corridor_thresh = np.ma.where(corridor_norm >= corridor_th_value, 1.0, 0.0)
|
|
510
|
-
clean_raster = algo_common.morph_raster(
|
|
511
|
-
corridor_thresh, in_canopy_r, exp_shk_cell, cell_size_x
|
|
512
|
-
)
|
|
575
|
+
clean_raster = algo_common.morph_raster(corridor_thresh, in_canopy_r, exp_shk_cell, cell_size_x)
|
|
513
576
|
|
|
514
577
|
# create mask for non-polygon area
|
|
515
578
|
mask = np.where(clean_raster == 1, True, False)
|
|
@@ -517,61 +580,35 @@ class LineInfo:
|
|
|
517
580
|
clean_raster = clean_raster.astype(np.int32)
|
|
518
581
|
|
|
519
582
|
# Process: ndarray to shapely Polygon
|
|
520
|
-
out_polygon = ras_feat.shapes(
|
|
521
|
-
clean_raster, mask=mask, transform=in_transform
|
|
522
|
-
)
|
|
583
|
+
out_polygon = ras_feat.shapes(clean_raster, mask=mask, transform=in_transform)
|
|
523
584
|
|
|
524
585
|
# create a shapely MultiPolygon
|
|
525
586
|
multi_polygon = []
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
587
|
+
if out_polygon is not None:
|
|
588
|
+
try:
|
|
589
|
+
for poly, value in out_polygon:
|
|
590
|
+
multi_polygon.append(sh_geom.shape(poly))
|
|
591
|
+
except TypeError:
|
|
592
|
+
pass
|
|
593
|
+
|
|
594
|
+
if not multi_polygon:
|
|
595
|
+
print("No polygons generated from raster. Returning None.")
|
|
596
|
+
return None
|
|
597
|
+
|
|
598
|
+
poly = sh_geom.MultiPolygon(multi_polygon) if multi_polygon else None
|
|
599
|
+
|
|
600
|
+
# create GeoDataFrame directly from dictionary
|
|
601
|
+
out_gdata = gpd.GeoDataFrame({"CorriThresh": [corridor_th_value], "geometry": [poly]})
|
|
602
|
+
out_gdata.set_geometry("geometry", inplace=True)
|
|
603
|
+
if shapefile_proj:
|
|
604
|
+
out_gdata = out_gdata.set_crs(shapefile_proj, allow_override=True)
|
|
605
|
+
|
|
606
|
+
if out_gdata is None or out_gdata.empty or out_gdata.geometry.isnull().all():
|
|
607
|
+
print("Empty GeoDataFrame from process_single_footprint.")
|
|
608
|
+
return None
|
|
540
609
|
|
|
541
610
|
return out_gdata
|
|
542
611
|
|
|
543
612
|
except Exception as e:
|
|
544
613
|
print("Exception: {}".format(e))
|
|
545
|
-
|
|
546
|
-
def line_footprint_rel(
|
|
547
|
-
in_line,
|
|
548
|
-
in_chm,
|
|
549
|
-
out_footprint,
|
|
550
|
-
processes,
|
|
551
|
-
verbose=True,
|
|
552
|
-
in_layer=None,
|
|
553
|
-
out_layer=None,
|
|
554
|
-
max_ln_width=32,
|
|
555
|
-
tree_radius=1.5,
|
|
556
|
-
max_line_dist=1.5,
|
|
557
|
-
canopy_avoidance=0.0,
|
|
558
|
-
exponent=1.0,
|
|
559
|
-
canopy_thresh_percentage=50,
|
|
560
|
-
parallel_mode=bt_const.ParallelMode.MULTIPROCESSING,
|
|
561
|
-
):
|
|
562
|
-
"""Another version of relative canopy footprint tool."""
|
|
563
|
-
footprint = FootprintCanopy(in_line, in_chm, in_layer)
|
|
564
|
-
footprint.compute(processes, parallel_mode)
|
|
565
|
-
|
|
566
|
-
# footprint.save_line_percentile(out_file_percentile)
|
|
567
|
-
footprint.save_footprint(out_footprint, out_layer)
|
|
568
|
-
|
|
569
|
-
if __name__ == "__main__":
|
|
570
|
-
"""This part is to be another version of relative canopy footprint tool."""
|
|
571
|
-
in_args, in_verbose = bt_common.check_arguments()
|
|
572
|
-
start_time = time.time()
|
|
573
|
-
line_footprint_rel(
|
|
574
|
-
**in_args.input, processes=int(in_args.processes), verbose=in_verbose
|
|
575
|
-
)
|
|
576
|
-
|
|
577
|
-
print("Elapsed time: {}".format(time.time() - start_time))
|
|
614
|
+
return None
|