BERATools 0.2.0__py3-none-any.whl → 0.2.2__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 +1 -7
- beratools/core/algo_centerline.py +491 -351
- beratools/core/algo_common.py +497 -0
- beratools/core/algo_cost.py +192 -0
- beratools/core/{dijkstra_algorithm.py → algo_dijkstra.py} +503 -460
- beratools/core/algo_footprint_rel.py +577 -0
- beratools/core/algo_line_grouping.py +944 -0
- beratools/core/algo_merge_lines.py +214 -0
- beratools/core/algo_split_with_lines.py +304 -0
- beratools/core/algo_tiler.py +428 -0
- beratools/core/algo_vertex_optimization.py +469 -0
- beratools/core/constants.py +52 -86
- beratools/core/logger.py +76 -85
- beratools/core/tool_base.py +196 -133
- beratools/gui/__init__.py +11 -15
- beratools/gui/{beratools.json → assets/beratools.json} +2185 -2300
- beratools/gui/batch_processing_dlg.py +513 -463
- beratools/gui/bt_data.py +481 -487
- beratools/gui/bt_gui_main.py +710 -691
- beratools/gui/main.py +26 -0
- beratools/gui/map_window.py +162 -146
- beratools/gui/tool_widgets.py +725 -493
- beratools/tools/Beratools_r_script.r +1120 -1120
- beratools/tools/Ht_metrics.py +116 -116
- beratools/tools/__init__.py +7 -7
- beratools/tools/batch_processing.py +136 -132
- beratools/tools/canopy_threshold_relative.py +672 -670
- beratools/tools/canopycostraster.py +222 -222
- beratools/tools/centerline.py +136 -176
- beratools/tools/common.py +857 -885
- beratools/tools/fl_regen_csf.py +428 -428
- beratools/tools/forest_line_attributes.py +408 -408
- beratools/tools/line_footprint_absolute.py +213 -363
- beratools/tools/line_footprint_fixed.py +436 -282
- beratools/tools/line_footprint_functions.py +733 -720
- beratools/tools/line_footprint_relative.py +73 -64
- beratools/tools/line_grouping.py +45 -0
- beratools/tools/ln_relative_metrics.py +615 -615
- beratools/tools/r_cal_lpi_elai.r +24 -24
- beratools/tools/r_generate_pd_focalraster.r +100 -100
- beratools/tools/r_interface.py +79 -79
- beratools/tools/r_point_density.r +8 -8
- beratools/tools/rpy_chm2trees.py +86 -86
- beratools/tools/rpy_dsm_chm_by.py +81 -81
- beratools/tools/rpy_dtm_by.py +63 -63
- beratools/tools/rpy_find_cellsize.py +43 -43
- beratools/tools/rpy_gnd_csf.py +74 -74
- beratools/tools/rpy_hummock_hollow.py +85 -85
- beratools/tools/rpy_hummock_hollow_raster.py +71 -71
- beratools/tools/rpy_las_info.py +51 -51
- beratools/tools/rpy_laz2las.py +40 -40
- beratools/tools/rpy_lpi_elai_lascat.py +466 -466
- beratools/tools/rpy_normalized_lidar_by.py +56 -56
- beratools/tools/rpy_percent_above_dbh.py +80 -80
- beratools/tools/rpy_points2trees.py +88 -88
- beratools/tools/rpy_vegcoverage.py +94 -94
- beratools/tools/tiler.py +48 -206
- beratools/tools/tool_template.py +69 -54
- beratools/tools/vertex_optimization.py +61 -620
- beratools/tools/zonal_threshold.py +144 -144
- beratools-0.2.2.dist-info/METADATA +108 -0
- beratools-0.2.2.dist-info/RECORD +74 -0
- {beratools-0.2.0.dist-info → beratools-0.2.2.dist-info}/WHEEL +1 -1
- {beratools-0.2.0.dist-info → beratools-0.2.2.dist-info}/licenses/LICENSE +22 -22
- beratools/gui/cli.py +0 -18
- beratools/gui/gui.json +0 -8
- beratools/gui_tk/ASCII Banners.txt +0 -248
- beratools/gui_tk/__init__.py +0 -20
- beratools/gui_tk/beratools_main.py +0 -515
- beratools/gui_tk/bt_widgets.py +0 -442
- beratools/gui_tk/cli.py +0 -18
- beratools/gui_tk/img/BERALogo.png +0 -0
- beratools/gui_tk/img/closed.gif +0 -0
- beratools/gui_tk/img/closed.png +0 -0
- beratools/gui_tk/img/open.gif +0 -0
- beratools/gui_tk/img/open.png +0 -0
- beratools/gui_tk/img/tool.gif +0 -0
- beratools/gui_tk/img/tool.png +0 -0
- beratools/gui_tk/main.py +0 -14
- beratools/gui_tk/map_window.py +0 -144
- beratools/gui_tk/runner.py +0 -1481
- beratools/gui_tk/tooltip.py +0 -55
- beratools/third_party/pyqtlet2/__init__.py +0 -9
- beratools/third_party/pyqtlet2/leaflet/__init__.py +0 -26
- beratools/third_party/pyqtlet2/leaflet/control/__init__.py +0 -6
- beratools/third_party/pyqtlet2/leaflet/control/control.py +0 -59
- beratools/third_party/pyqtlet2/leaflet/control/draw.py +0 -52
- beratools/third_party/pyqtlet2/leaflet/control/layers.py +0 -20
- beratools/third_party/pyqtlet2/leaflet/core/Parser.py +0 -24
- beratools/third_party/pyqtlet2/leaflet/core/__init__.py +0 -2
- beratools/third_party/pyqtlet2/leaflet/core/evented.py +0 -180
- beratools/third_party/pyqtlet2/leaflet/layer/__init__.py +0 -5
- beratools/third_party/pyqtlet2/leaflet/layer/featuregroup.py +0 -34
- beratools/third_party/pyqtlet2/leaflet/layer/icon/__init__.py +0 -1
- beratools/third_party/pyqtlet2/leaflet/layer/icon/icon.py +0 -30
- beratools/third_party/pyqtlet2/leaflet/layer/imageoverlay.py +0 -18
- beratools/third_party/pyqtlet2/leaflet/layer/layer.py +0 -105
- beratools/third_party/pyqtlet2/leaflet/layer/layergroup.py +0 -45
- beratools/third_party/pyqtlet2/leaflet/layer/marker/__init__.py +0 -1
- beratools/third_party/pyqtlet2/leaflet/layer/marker/marker.py +0 -91
- beratools/third_party/pyqtlet2/leaflet/layer/tile/__init__.py +0 -2
- beratools/third_party/pyqtlet2/leaflet/layer/tile/gridlayer.py +0 -4
- beratools/third_party/pyqtlet2/leaflet/layer/tile/tilelayer.py +0 -16
- beratools/third_party/pyqtlet2/leaflet/layer/vector/__init__.py +0 -5
- beratools/third_party/pyqtlet2/leaflet/layer/vector/circle.py +0 -15
- beratools/third_party/pyqtlet2/leaflet/layer/vector/circlemarker.py +0 -18
- beratools/third_party/pyqtlet2/leaflet/layer/vector/path.py +0 -5
- beratools/third_party/pyqtlet2/leaflet/layer/vector/polygon.py +0 -14
- beratools/third_party/pyqtlet2/leaflet/layer/vector/polyline.py +0 -18
- beratools/third_party/pyqtlet2/leaflet/layer/vector/rectangle.py +0 -14
- beratools/third_party/pyqtlet2/leaflet/map/__init__.py +0 -1
- beratools/third_party/pyqtlet2/leaflet/map/map.py +0 -220
- beratools/third_party/pyqtlet2/mapwidget.py +0 -45
- beratools/third_party/pyqtlet2/web/custom.js +0 -43
- beratools/third_party/pyqtlet2/web/map.html +0 -23
- beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/layers-2x.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/layers.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/marker-icon-2x.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/marker-icon.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/marker-shadow.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_193/leaflet.css +0 -656
- beratools/third_party/pyqtlet2/web/modules/leaflet_193/leaflet.js +0 -6
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.codeclimate.yml +0 -14
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.editorconfig +0 -4
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.gitattributes +0 -22
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.travis.yml +0 -43
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/LICENSE +0 -20
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/layers-2x.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/layers.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/marker-icon-2x.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/marker-icon.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/marker-shadow.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/spritesheet-2x.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/spritesheet.png +0 -0
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/spritesheet.svg +0 -156
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/leaflet.draw.css +0 -10
- beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/leaflet.draw.js +0 -10
- beratools/third_party/pyqtlet2/web/modules/leaflet_rotatedMarker_020/LICENSE +0 -22
- beratools/third_party/pyqtlet2/web/modules/leaflet_rotatedMarker_020/leaflet.rotatedMarker.js +0 -57
- beratools/tools/forest_line_ecosite.py +0 -216
- beratools/tools/lapis_all.py +0 -103
- beratools/tools/least_cost_path_from_chm.py +0 -152
- beratools-0.2.0.dist-info/METADATA +0 -63
- beratools-0.2.0.dist-info/RECORD +0 -142
- /beratools/gui/{img → assets}/BERALogo.png +0 -0
- /beratools/gui/{img → assets}/closed.gif +0 -0
- /beratools/gui/{img → assets}/closed.png +0 -0
- /beratools/{gui_tk → gui/assets}/gui.json +0 -0
- /beratools/gui/{img → assets}/open.gif +0 -0
- /beratools/gui/{img → assets}/open.png +0 -0
- /beratools/gui/{img → assets}/tool.gif +0 -0
- /beratools/gui/{img → assets}/tool.png +0 -0
- {beratools-0.2.0.dist-info → beratools-0.2.2.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,577 @@
|
|
|
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, Maverick Fong
|
|
8
|
+
|
|
9
|
+
Description:
|
|
10
|
+
This script is part of the BERA Tools.
|
|
11
|
+
Webpage: https://github.com/appliedgrg/beratools
|
|
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.
|
|
15
|
+
"""
|
|
16
|
+
import math
|
|
17
|
+
import time
|
|
18
|
+
from enum import StrEnum
|
|
19
|
+
|
|
20
|
+
import geopandas as gpd
|
|
21
|
+
import numpy as np
|
|
22
|
+
import pandas as pd
|
|
23
|
+
import rasterio.features as ras_feat
|
|
24
|
+
import shapely
|
|
25
|
+
import shapely.geometry as sh_geom
|
|
26
|
+
import shapely.ops as sh_ops
|
|
27
|
+
from skimage.graph import MCP_Flexible
|
|
28
|
+
|
|
29
|
+
import beratools.core.algo_common as algo_common
|
|
30
|
+
import beratools.core.algo_cost as algo_cost
|
|
31
|
+
import beratools.core.constants as bt_const
|
|
32
|
+
import beratools.core.tool_base as bt_base
|
|
33
|
+
import beratools.tools.common as bt_common
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Side(StrEnum):
|
|
37
|
+
"""Constants for left and right side."""
|
|
38
|
+
|
|
39
|
+
left = "left"
|
|
40
|
+
right = "right"
|
|
41
|
+
|
|
42
|
+
class FootprintCanopy:
|
|
43
|
+
"""Relative canopy footprint class."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, in_geom, in_chm, in_layer=None):
|
|
46
|
+
data = gpd.read_file(in_geom, layer=in_layer)
|
|
47
|
+
self.lines = []
|
|
48
|
+
|
|
49
|
+
for idx in data.index:
|
|
50
|
+
line = LineInfo(data.iloc[[idx]], in_chm)
|
|
51
|
+
self.lines.append(line)
|
|
52
|
+
|
|
53
|
+
def compute(self, processes, parallel_mode=bt_const.ParallelMode.MULTIPROCESSING):
|
|
54
|
+
result = bt_base.execute_multiprocessing(
|
|
55
|
+
algo_common.process_single_item,
|
|
56
|
+
self.lines,
|
|
57
|
+
"Canopy Footprint",
|
|
58
|
+
processes,
|
|
59
|
+
1,
|
|
60
|
+
parallel_mode,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
fp = [item.footprint for item in result]
|
|
64
|
+
self.footprints = pd.concat(fp)
|
|
65
|
+
|
|
66
|
+
percentile = [item.line for item in result]
|
|
67
|
+
self.lines_percentile = pd.concat(percentile)
|
|
68
|
+
|
|
69
|
+
def save_footprint(self, out_footprint, layer=None):
|
|
70
|
+
self.footprints.to_file(out_footprint, layer=layer)
|
|
71
|
+
|
|
72
|
+
def save_line_percentile(self, out_percentile):
|
|
73
|
+
self.lines_percentile.to_file(out_percentile)
|
|
74
|
+
|
|
75
|
+
class BufferRing:
|
|
76
|
+
"""Buffer ring class."""
|
|
77
|
+
|
|
78
|
+
def __init__(self, ring_poly, side):
|
|
79
|
+
self.geometry = ring_poly
|
|
80
|
+
self.side = side
|
|
81
|
+
self.percentile = 0.5
|
|
82
|
+
self.Dyn_Canopy_Threshold = 0.05
|
|
83
|
+
|
|
84
|
+
class LineInfo:
|
|
85
|
+
"""Class to store line information."""
|
|
86
|
+
|
|
87
|
+
def __init__(self,
|
|
88
|
+
line_gdf, in_chm,
|
|
89
|
+
max_ln_width=32,
|
|
90
|
+
tree_radius=1.5,
|
|
91
|
+
max_line_dist=1.5,
|
|
92
|
+
canopy_avoidance=0.0,
|
|
93
|
+
exponent=1.0,
|
|
94
|
+
canopy_thresh_percentage=50):
|
|
95
|
+
self.line = line_gdf
|
|
96
|
+
self.in_chm = in_chm
|
|
97
|
+
self.line_simp = self.line.geometry.simplify(
|
|
98
|
+
tolerance=0.5, preserve_topology=True
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
self.canopy_percentile = 50
|
|
102
|
+
self.DynCanTh = np.nan
|
|
103
|
+
# chk_df_multipart
|
|
104
|
+
# if proc_segments:
|
|
105
|
+
# line_seg = split_into_segments(line_seg)
|
|
106
|
+
|
|
107
|
+
self.buffer_rings = []
|
|
108
|
+
|
|
109
|
+
self.CL_CutHt = np.nan
|
|
110
|
+
self.CR_CutHt = np.nan
|
|
111
|
+
self.RDist_Cut = np.nan
|
|
112
|
+
self.LDist_Cut = np.nan
|
|
113
|
+
|
|
114
|
+
self.canopy_thresh_percentage = canopy_thresh_percentage
|
|
115
|
+
self.canopy_avoidance = canopy_avoidance
|
|
116
|
+
self.exponent = exponent
|
|
117
|
+
self.max_ln_width = max_ln_width
|
|
118
|
+
self.max_line_dist = max_line_dist
|
|
119
|
+
self.tree_radius = tree_radius
|
|
120
|
+
|
|
121
|
+
self.nodata = -9999
|
|
122
|
+
self.dyn_canopy_ndarray = None
|
|
123
|
+
self.negative_cost_clip = None
|
|
124
|
+
self.out_meta = None
|
|
125
|
+
|
|
126
|
+
self.buffer_left = None
|
|
127
|
+
self.buffer_right = None
|
|
128
|
+
self.footprint = None
|
|
129
|
+
|
|
130
|
+
def compute(self):
|
|
131
|
+
self.prepare_ring_buffer()
|
|
132
|
+
|
|
133
|
+
ring_list = []
|
|
134
|
+
for item in self.buffer_rings:
|
|
135
|
+
ring = self.cal_percentileRing(item)
|
|
136
|
+
ring_list.append(ring)
|
|
137
|
+
|
|
138
|
+
self.buffer_rings = ring_list
|
|
139
|
+
|
|
140
|
+
self.rate_of_change(self.get_percentile_array(Side.left), Side.left)
|
|
141
|
+
self.rate_of_change(self.get_percentile_array(Side.right), Side.right)
|
|
142
|
+
|
|
143
|
+
self.line["CL_CutHt"] = self.CL_CutHt
|
|
144
|
+
self.line["CR_CutHt"] = self.CR_CutHt
|
|
145
|
+
self.line["RDist_Cut"] = self.RDist_Cut
|
|
146
|
+
self.line["LDist_Cut"] = self.LDist_Cut
|
|
147
|
+
|
|
148
|
+
self.DynCanTh = (self.CL_CutHt + self.CR_CutHt) / 2
|
|
149
|
+
self.line["DynCanTh"] = self.DynCanTh
|
|
150
|
+
|
|
151
|
+
self.prepare_line_buffer()
|
|
152
|
+
|
|
153
|
+
fp_left = self.process_single_footprint(Side.left)
|
|
154
|
+
fp_right = self.process_single_footprint(Side.right)
|
|
155
|
+
fp_left.geometry = fp_left.buffer(0.005)
|
|
156
|
+
fp_right.geometry = fp_right.buffer(0.005)
|
|
157
|
+
self.footprint = pd.concat([fp_left, fp_right])
|
|
158
|
+
self.footprint = self.footprint.dissolve()
|
|
159
|
+
self.footprint.geometry = self.footprint.buffer(-0.005)
|
|
160
|
+
|
|
161
|
+
# transfer group value to footprint
|
|
162
|
+
# TODO: lines and footprint can match by index
|
|
163
|
+
if bt_const.BT_GROUP in self.line.columns:
|
|
164
|
+
self.footprint[bt_const.BT_GROUP] = self.line[bt_const.BT_GROUP].iloc[0]
|
|
165
|
+
|
|
166
|
+
def prepare_ring_buffer(self):
|
|
167
|
+
nrings = 1
|
|
168
|
+
ringdist = 15
|
|
169
|
+
ring_list = self.multi_ring_buffer(self.line_simp, nrings, ringdist)
|
|
170
|
+
for i in ring_list:
|
|
171
|
+
if BufferRing(i, Side.left):
|
|
172
|
+
self.buffer_rings.append(BufferRing(i, Side.left))
|
|
173
|
+
else:
|
|
174
|
+
print("Empty buffer ring")
|
|
175
|
+
|
|
176
|
+
nrings = -1
|
|
177
|
+
ringdist = -15
|
|
178
|
+
ring_list = self.multi_ring_buffer(self.line_simp, nrings, ringdist)
|
|
179
|
+
for i in ring_list:
|
|
180
|
+
if BufferRing(i, Side.right):
|
|
181
|
+
self.buffer_rings.append(BufferRing(i, Side.right))
|
|
182
|
+
else:
|
|
183
|
+
print("Empty buffer ring")
|
|
184
|
+
|
|
185
|
+
def cal_percentileRing(self, ring):
|
|
186
|
+
try:
|
|
187
|
+
line_buffer = ring.geometry
|
|
188
|
+
if line_buffer.is_empty or shapely.is_missing(line_buffer):
|
|
189
|
+
return None
|
|
190
|
+
if line_buffer.has_z:
|
|
191
|
+
line_buffer = sh_ops.transform(
|
|
192
|
+
lambda x, y, z=None: (x, y), line_buffer
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
except Exception as e:
|
|
196
|
+
print(f"cal_percentileRing: {e}")
|
|
197
|
+
|
|
198
|
+
# TODO: temporary workaround for exception causing not percentile defined
|
|
199
|
+
try:
|
|
200
|
+
clipped_raster, _ = bt_common.clip_raster(self.in_chm, line_buffer, 0)
|
|
201
|
+
clipped_raster = np.squeeze(clipped_raster, axis=0)
|
|
202
|
+
|
|
203
|
+
# mask all -9999 (nodata) value cells
|
|
204
|
+
masked_raster = np.ma.masked_where(
|
|
205
|
+
clipped_raster == bt_const.BT_NODATA, clipped_raster
|
|
206
|
+
)
|
|
207
|
+
filled_raster = np.ma.filled(masked_raster, np.nan)
|
|
208
|
+
|
|
209
|
+
# Calculate the percentile
|
|
210
|
+
percentile = np.nanpercentile(filled_raster, 50)
|
|
211
|
+
|
|
212
|
+
if percentile > 1:
|
|
213
|
+
ring.Dyn_Canopy_Threshold = percentile * (0.3)
|
|
214
|
+
else:
|
|
215
|
+
ring.Dyn_Canopy_Threshold = 1
|
|
216
|
+
|
|
217
|
+
ring.percentile = percentile
|
|
218
|
+
except Exception as e:
|
|
219
|
+
print(e)
|
|
220
|
+
print("Default values are used.")
|
|
221
|
+
|
|
222
|
+
return ring
|
|
223
|
+
|
|
224
|
+
def get_percentile_array(self, side):
|
|
225
|
+
per_array = []
|
|
226
|
+
for item in self.buffer_rings:
|
|
227
|
+
try:
|
|
228
|
+
if item.side == side:
|
|
229
|
+
per_array.append(item.percentile)
|
|
230
|
+
except Exception as e:
|
|
231
|
+
print(e)
|
|
232
|
+
|
|
233
|
+
return per_array
|
|
234
|
+
|
|
235
|
+
def rate_of_change(self, percentile_array, side):
|
|
236
|
+
# Since the x interval is 1 unit, the array 'diff' is the rate of change (slope)
|
|
237
|
+
diff = np.ediff1d(percentile_array)
|
|
238
|
+
cut_dist = len(percentile_array) / 5
|
|
239
|
+
|
|
240
|
+
median_percentile = np.nanmedian(percentile_array)
|
|
241
|
+
if not np.isnan(median_percentile):
|
|
242
|
+
cut_percentile = float(math.floor(median_percentile))
|
|
243
|
+
else:
|
|
244
|
+
cut_percentile = 0.5
|
|
245
|
+
|
|
246
|
+
found = False
|
|
247
|
+
changes = 1.50
|
|
248
|
+
Change = np.insert(diff, 0, 0)
|
|
249
|
+
scale_down = 1.0
|
|
250
|
+
|
|
251
|
+
# test the rate of change is > than 150% (1.5), if it is
|
|
252
|
+
# no result found then lower to 140% (1.4) until 110% (1.1)
|
|
253
|
+
try:
|
|
254
|
+
while not found and changes >= 1.1:
|
|
255
|
+
for ii in range(0, len(Change) - 1):
|
|
256
|
+
if percentile_array[ii] >= 0.5:
|
|
257
|
+
if (Change[ii]) >= changes:
|
|
258
|
+
cut_dist = (ii + 1) * scale_down
|
|
259
|
+
cut_percentile = math.floor(percentile_array[ii])
|
|
260
|
+
|
|
261
|
+
if 0.5 >= cut_percentile:
|
|
262
|
+
if cut_dist > 5:
|
|
263
|
+
cut_percentile = 2
|
|
264
|
+
cut_dist = cut_dist * scale_down**3
|
|
265
|
+
# @<0.5 found and modified
|
|
266
|
+
elif 0.5 < cut_percentile <= 5.0:
|
|
267
|
+
if cut_dist > 6:
|
|
268
|
+
cut_dist = cut_dist * scale_down**3 # 4.0
|
|
269
|
+
# @0.5-5.0 found and modified
|
|
270
|
+
elif 5.0 < cut_percentile <= 10.0:
|
|
271
|
+
if cut_dist > 8: # 5
|
|
272
|
+
cut_dist = cut_dist * scale_down**3
|
|
273
|
+
# @5-10 found and modified
|
|
274
|
+
elif 10.0 < cut_percentile <= 15:
|
|
275
|
+
if cut_dist > 5:
|
|
276
|
+
cut_dist = cut_dist * scale_down**3 # 5.5
|
|
277
|
+
# @10-15 found and modified
|
|
278
|
+
elif 15 < cut_percentile:
|
|
279
|
+
if cut_dist > 4:
|
|
280
|
+
cut_dist = cut_dist * scale_down**2
|
|
281
|
+
cut_percentile = 15.5
|
|
282
|
+
# @>15 found and modified
|
|
283
|
+
found = True
|
|
284
|
+
# rate of change found
|
|
285
|
+
break
|
|
286
|
+
changes = changes - 0.1
|
|
287
|
+
|
|
288
|
+
except IndexError:
|
|
289
|
+
pass
|
|
290
|
+
|
|
291
|
+
# if still no result found, lower to 10% (1.1),
|
|
292
|
+
# if no result found then default is used
|
|
293
|
+
if not found:
|
|
294
|
+
if 0.5 >= median_percentile:
|
|
295
|
+
cut_dist = 4 * scale_down # 3
|
|
296
|
+
cut_percentile = 0.5
|
|
297
|
+
elif 0.5 < median_percentile <= 5.0:
|
|
298
|
+
cut_dist = 4.5 * scale_down # 4.0
|
|
299
|
+
cut_percentile = math.floor(median_percentile)
|
|
300
|
+
elif 5.0 < median_percentile <= 10.0:
|
|
301
|
+
cut_dist = 5.5 * scale_down # 5
|
|
302
|
+
cut_percentile = math.floor(median_percentile)
|
|
303
|
+
elif 10.0 < median_percentile <= 15:
|
|
304
|
+
cut_dist = 6 * scale_down # 5.5
|
|
305
|
+
cut_percentile = math.floor(median_percentile)
|
|
306
|
+
elif 15 < median_percentile:
|
|
307
|
+
cut_dist = 5 * scale_down # 5
|
|
308
|
+
cut_percentile = 15.5
|
|
309
|
+
|
|
310
|
+
if side == Side.right:
|
|
311
|
+
self.RDist_Cut = cut_dist
|
|
312
|
+
self.CR_CutHt = float(cut_percentile)
|
|
313
|
+
elif side == Side.left:
|
|
314
|
+
self.LDist_Cut = cut_dist
|
|
315
|
+
self.CL_CutHt = float(cut_percentile)
|
|
316
|
+
|
|
317
|
+
def multi_ring_buffer(self, df, nrings, ringdist):
|
|
318
|
+
"""
|
|
319
|
+
Buffers an input DataFrames geometry nring (number of rings) times.
|
|
320
|
+
|
|
321
|
+
Compute with a distance between rings of ringdist and returns
|
|
322
|
+
a list of non overlapping buffers
|
|
323
|
+
"""
|
|
324
|
+
rings = [] # A list to hold the individual buffers
|
|
325
|
+
line = df.geometry.iloc[0]
|
|
326
|
+
# For each ring (1, 2, 3, ..., nrings)
|
|
327
|
+
for ring in np.arange(0, ringdist, nrings):
|
|
328
|
+
big_ring = line.buffer(
|
|
329
|
+
nrings + ring, single_sided=True, cap_style="flat"
|
|
330
|
+
) # Create one big buffer
|
|
331
|
+
small_ring = line.buffer(
|
|
332
|
+
ring, single_sided=True, cap_style="flat"
|
|
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
|
|
337
|
+
if (
|
|
338
|
+
~shapely.is_empty(the_ring)
|
|
339
|
+
or ~shapely.is_missing(the_ring)
|
|
340
|
+
or not None
|
|
341
|
+
or ~the_ring.area == 0
|
|
342
|
+
):
|
|
343
|
+
if isinstance(the_ring, sh_geom.MultiPolygon) or isinstance(
|
|
344
|
+
the_ring, shapely.Polygon
|
|
345
|
+
):
|
|
346
|
+
rings.append(the_ring) # Append the ring to the rings list
|
|
347
|
+
else:
|
|
348
|
+
if isinstance(the_ring, shapely.GeometryCollection):
|
|
349
|
+
for i in range(0, len(the_ring.geoms)):
|
|
350
|
+
if not isinstance(the_ring.geoms[i], shapely.LineString):
|
|
351
|
+
rings.append(the_ring.geoms[i])
|
|
352
|
+
|
|
353
|
+
return rings # return the list
|
|
354
|
+
|
|
355
|
+
def prepare_line_buffer(self):
|
|
356
|
+
line = self.line.geometry.iloc[0]
|
|
357
|
+
buffer_left_1 = line.buffer(
|
|
358
|
+
distance=self.max_ln_width + 1,
|
|
359
|
+
cap_style=3,
|
|
360
|
+
single_sided=True,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
buffer_left_2 = line.buffer(
|
|
364
|
+
distance=-1,
|
|
365
|
+
cap_style=3,
|
|
366
|
+
single_sided=True,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
self.buffer_left = sh_ops.unary_union([buffer_left_1, buffer_left_2])
|
|
370
|
+
|
|
371
|
+
buffer_right_1 = line.buffer(
|
|
372
|
+
distance=-self.max_ln_width - 1,
|
|
373
|
+
cap_style=3,
|
|
374
|
+
single_sided=True,
|
|
375
|
+
)
|
|
376
|
+
buffer_right_2 = line.buffer(distance=1, cap_style=3, single_sided=True)
|
|
377
|
+
|
|
378
|
+
self.buffer_right = sh_ops.unary_union([buffer_right_1, buffer_right_2])
|
|
379
|
+
|
|
380
|
+
def dyn_canopy_cost_raster(self, side):
|
|
381
|
+
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
|
+
line_df = self.line
|
|
387
|
+
out_meta = self.out_meta
|
|
388
|
+
|
|
389
|
+
canopy_thresh_percentage = self.canopy_thresh_percentage / 100
|
|
390
|
+
|
|
391
|
+
if side == Side.left:
|
|
392
|
+
canopy_ht_threshold = line_df.CL_CutHt * canopy_thresh_percentage
|
|
393
|
+
Cut_Dist = self.LDist_Cut
|
|
394
|
+
line_buffer = self.buffer_left
|
|
395
|
+
elif side == Side.right:
|
|
396
|
+
canopy_ht_threshold = line_df.CR_CutHt * canopy_thresh_percentage
|
|
397
|
+
Cut_Dist = self.RDist_Cut
|
|
398
|
+
line_buffer = self.buffer_right
|
|
399
|
+
else:
|
|
400
|
+
canopy_ht_threshold = 0.5
|
|
401
|
+
|
|
402
|
+
canopy_ht_threshold = float(canopy_ht_threshold)
|
|
403
|
+
if canopy_ht_threshold <= 0:
|
|
404
|
+
canopy_ht_threshold = 0.5
|
|
405
|
+
|
|
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
|
+
try:
|
|
413
|
+
clipped_rasterC, out_meta = bt_common.clip_raster(
|
|
414
|
+
in_chm_raster, line_buffer, 0
|
|
415
|
+
)
|
|
416
|
+
negative_cost_clip, dyn_canopy_ndarray = algo_cost.cost_raster(
|
|
417
|
+
clipped_rasterC,
|
|
418
|
+
out_meta,
|
|
419
|
+
self.tree_radius,
|
|
420
|
+
canopy_ht_threshold,
|
|
421
|
+
self.max_line_dist,
|
|
422
|
+
self.canopy_avoidance,
|
|
423
|
+
self.exponent,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
return dyn_canopy_ndarray, negative_cost_clip, out_meta, Cut_Dist
|
|
427
|
+
|
|
428
|
+
except Exception as e:
|
|
429
|
+
print(f"dyn_canopy_cost_raster: {e}")
|
|
430
|
+
return None
|
|
431
|
+
|
|
432
|
+
def process_single_footprint(self, side):
|
|
433
|
+
# this will change segment content, and parameters will be changed
|
|
434
|
+
in_canopy_r, in_cost_r, in_meta, Cut_Dist = self.dyn_canopy_cost_raster(side)
|
|
435
|
+
|
|
436
|
+
if np.isnan(in_canopy_r).all():
|
|
437
|
+
print("Canopy raster empty")
|
|
438
|
+
|
|
439
|
+
if np.isnan(in_cost_r).all():
|
|
440
|
+
print("Cost raster empty")
|
|
441
|
+
|
|
442
|
+
exp_shk_cell = self.exponent # TODO: duplicate vars
|
|
443
|
+
no_data = self.nodata
|
|
444
|
+
|
|
445
|
+
shapefile_proj = self.line.crs
|
|
446
|
+
in_transform = in_meta["transform"]
|
|
447
|
+
|
|
448
|
+
segment_list = []
|
|
449
|
+
|
|
450
|
+
feat = self.line.geometry.iloc[0]
|
|
451
|
+
for coord in feat.coords:
|
|
452
|
+
segment_list.append(coord)
|
|
453
|
+
|
|
454
|
+
cell_size_x = in_transform[0]
|
|
455
|
+
cell_size_y = -in_transform[4]
|
|
456
|
+
|
|
457
|
+
# Work out the corridor from both end of the centerline
|
|
458
|
+
try:
|
|
459
|
+
if len(in_cost_r.shape) > 2:
|
|
460
|
+
in_cost_r = np.squeeze(in_cost_r, axis=0)
|
|
461
|
+
|
|
462
|
+
algo_cost.remove_nan_from_array_refactor(in_cost_r)
|
|
463
|
+
in_cost_r[in_cost_r == no_data] = np.inf
|
|
464
|
+
|
|
465
|
+
# generate 1m interval points along line
|
|
466
|
+
distance_delta = 1
|
|
467
|
+
distances = np.arange(0, feat.length, distance_delta)
|
|
468
|
+
multipoint_along_line = [
|
|
469
|
+
feat.interpolate(distance) for distance in distances
|
|
470
|
+
]
|
|
471
|
+
multipoint_along_line.append(sh_geom.Point(segment_list[-1]))
|
|
472
|
+
# Rasterize points along line
|
|
473
|
+
rasterized_points_Alongln = ras_feat.rasterize(
|
|
474
|
+
multipoint_along_line,
|
|
475
|
+
out_shape=in_cost_r.shape,
|
|
476
|
+
transform=in_transform,
|
|
477
|
+
fill=0,
|
|
478
|
+
all_touched=True,
|
|
479
|
+
default_value=1,
|
|
480
|
+
)
|
|
481
|
+
points_Alongln = np.transpose(np.nonzero(rasterized_points_Alongln))
|
|
482
|
+
|
|
483
|
+
# Find minimum cost paths through an N-d costs array.
|
|
484
|
+
mcp_flexible1 = MCP_Flexible(
|
|
485
|
+
in_cost_r, sampling=(cell_size_x, cell_size_y), fully_connected=True
|
|
486
|
+
)
|
|
487
|
+
flex_cost_alongLn, flex_back_alongLn = mcp_flexible1.find_costs(
|
|
488
|
+
starts=points_Alongln
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
# Generate corridor
|
|
492
|
+
corridor = flex_cost_alongLn
|
|
493
|
+
corridor = np.ma.masked_invalid(corridor)
|
|
494
|
+
|
|
495
|
+
# Calculate minimum value of corridor raster
|
|
496
|
+
if np.ma.min(corridor) is not None:
|
|
497
|
+
corr_min = float(np.ma.min(corridor))
|
|
498
|
+
else:
|
|
499
|
+
corr_min = 0.5
|
|
500
|
+
|
|
501
|
+
# normalize corridor raster by deducting corr_min
|
|
502
|
+
corridor_norm = corridor - corr_min
|
|
503
|
+
|
|
504
|
+
# Set minimum as zero and save minimum file
|
|
505
|
+
corridor_th_value = Cut_Dist / cell_size_x
|
|
506
|
+
if corridor_th_value < 0: # if no threshold found, use default value
|
|
507
|
+
corridor_th_value = bt_const.FP_CORRIDOR_THRESHOLD / cell_size_x
|
|
508
|
+
|
|
509
|
+
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
|
+
)
|
|
513
|
+
|
|
514
|
+
# create mask for non-polygon area
|
|
515
|
+
mask = np.where(clean_raster == 1, True, False)
|
|
516
|
+
if clean_raster.dtype == np.int64:
|
|
517
|
+
clean_raster = clean_raster.astype(np.int32)
|
|
518
|
+
|
|
519
|
+
# Process: ndarray to shapely Polygon
|
|
520
|
+
out_polygon = ras_feat.shapes(
|
|
521
|
+
clean_raster, mask=mask, transform=in_transform
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
# create a shapely MultiPolygon
|
|
525
|
+
multi_polygon = []
|
|
526
|
+
for poly, value in out_polygon:
|
|
527
|
+
multi_polygon.append(sh_geom.shape(poly))
|
|
528
|
+
poly = sh_geom.MultiPolygon(multi_polygon)
|
|
529
|
+
|
|
530
|
+
# create a pandas DataFrame for the FP
|
|
531
|
+
out_data = pd.DataFrame(
|
|
532
|
+
{
|
|
533
|
+
"CorriThresh": [corridor_th_value],
|
|
534
|
+
"geometry": [poly]
|
|
535
|
+
}
|
|
536
|
+
)
|
|
537
|
+
out_gdata = gpd.GeoDataFrame(
|
|
538
|
+
out_data, geometry="geometry", crs=shapefile_proj
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
return out_gdata
|
|
542
|
+
|
|
543
|
+
except Exception as e:
|
|
544
|
+
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))
|