BERATools 0.3.7__tar.gz → 0.3.9__tar.gz
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-0.3.7 → beratools-0.3.9}/PKG-INFO +4 -1
- {beratools-0.3.7 → beratools-0.3.9}/beratools/__init__.py +1 -1
- beratools-0.3.9/beratools/core/algo_check_seed_line.py +172 -0
- beratools-0.3.9/beratools/core/algo_seed_line_correction.py +606 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/core/algo_vertex_preclean.py +56 -2
- {beratools-0.3.7 → beratools-0.3.9}/beratools/core/canopy_threshold_relative.py +49 -160
- {beratools-0.3.7 → beratools-0.3.9}/beratools/core/line_footprint_functions.py +1 -2
- {beratools-0.3.7 → beratools-0.3.9}/beratools/core/tool_base.py +1 -1
- {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/assets/beratools.json +113 -2
- {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/bt_data.py +23 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/bt_gui_main.py +44 -12
- {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/tool_widgets.py +107 -33
- {beratools-0.3.7 → beratools-0.3.9}/beratools/tools/centerline.py +1 -1
- {beratools-0.3.7 → beratools-0.3.9}/beratools/tools/check_seed_line.py +652 -85
- {beratools-0.3.7 → beratools-0.3.9}/beratools/tools/line_footprint_relative.py +2 -2
- {beratools-0.3.7 → beratools-0.3.9}/beratools/tools/vertex_optimization.py +1 -1
- {beratools-0.3.7 → beratools-0.3.9}/beratools/utility/spatial_common.py +5 -2
- {beratools-0.3.7 → beratools-0.3.9}/pyproject.toml +3 -0
- {beratools-0.3.7 → beratools-0.3.9}/.gitignore +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/LICENSE +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/README.md +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/cli/__init__.py +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/cli/entry.py +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/core/__init__.py +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/core/algo_canopy_footprint_absolute.py +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/core/algo_centerline.py +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/core/algo_common.py +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/core/algo_cost.py +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/core/algo_dijkstra.py +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/core/algo_line_grouping.py +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/core/algo_merge_lines.py +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/core/algo_split_with_lines.py +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/core/algo_vertex_optimization.py +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/core/constants.py +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/core/logger.py +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/external/__init__.py +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/external/polygon_centerline/LICENSE.md +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/external/polygon_centerline/UPSTREAM.md +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/external/polygon_centerline/__init__.py +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/external/polygon_centerline/_src.py +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/external/polygon_centerline/exceptions.py +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/assets/BERALogo.ico +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/assets/BERALogo.png +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/assets/BERA_GUI_Main.png +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/assets/BERA_Splash.gif +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/assets/BERA_WizardImage.png +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/assets/closed.gif +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/assets/closed.png +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/assets/gui.json +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/assets/open.gif +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/assets/open.png +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/assets/tool.gif +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/assets/tool.png +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/geometry_types.py +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/gui/main.py +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/tools/__init__.py +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/tools/canopy_footprint_absolute.py +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/tools/common.py +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/tools/ground_footprint.py +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/tools/tool_template.py +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/utility/env_checks.py +0 -0
- {beratools-0.3.7 → beratools-0.3.9}/beratools/utility/tool_args.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: BERATools
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.9
|
|
4
4
|
Summary: An advanced forest line feature analysis platform
|
|
5
5
|
Project-URL: Homepage, https://github.com/appliedgrg/beratools
|
|
6
6
|
Author-email: AppliedGRG <appliedgrg@gmail.com>, Richard Zeng <richardqzeng@gmail.com>
|
|
@@ -20,6 +20,7 @@ Requires-Python: >=3.10
|
|
|
20
20
|
Requires-Dist: gdal; platform_system != 'Windows'
|
|
21
21
|
Requires-Dist: geopandas
|
|
22
22
|
Requires-Dist: networkit
|
|
23
|
+
Requires-Dist: pandarallel
|
|
23
24
|
Requires-Dist: pyogrio>=0.9.0
|
|
24
25
|
Requires-Dist: pyqt5
|
|
25
26
|
Requires-Dist: rasterio
|
|
@@ -49,6 +50,8 @@ Requires-Dist: pymdown-extensions; extra == 'doc'
|
|
|
49
50
|
Provides-Extra: test
|
|
50
51
|
Requires-Dist: gdal==3.8.4; extra == 'test'
|
|
51
52
|
Requires-Dist: pytest; extra == 'test'
|
|
53
|
+
Requires-Dist: pytest-qt; extra == 'test'
|
|
54
|
+
Requires-Dist: pywinauto; (platform_system == 'Windows') and extra == 'test'
|
|
52
55
|
Description-Content-Type: text/markdown
|
|
53
56
|
|
|
54
57
|
# BERA Tools
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Per-line vertex preclean helpers for line optimization workflows."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
import geopandas as gpd
|
|
6
|
+
import shapely.geometry as sh_geom
|
|
7
|
+
|
|
8
|
+
import beratools.core.constants as bt_const
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class VertexPrecleaner:
|
|
12
|
+
VALID_MODES = {"full", "endpoint_only"}
|
|
13
|
+
|
|
14
|
+
def __init__(self, close_distance, angle_tol):
|
|
15
|
+
self.close_distance = float(close_distance)
|
|
16
|
+
self.angle_tol = float(angle_tol)
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def _point_distance(coord_a, coord_b):
|
|
20
|
+
return math.hypot(coord_b[0] - coord_a[0], coord_b[1] - coord_a[1])
|
|
21
|
+
|
|
22
|
+
def _bend_score(self, coord_a, coord_b, coord_c):
|
|
23
|
+
v1x = coord_b[0] - coord_a[0]
|
|
24
|
+
v1y = coord_b[1] - coord_a[1]
|
|
25
|
+
v2x = coord_c[0] - coord_b[0]
|
|
26
|
+
v2y = coord_c[1] - coord_b[1]
|
|
27
|
+
|
|
28
|
+
norm1 = math.hypot(v1x, v1y)
|
|
29
|
+
norm2 = math.hypot(v2x, v2y)
|
|
30
|
+
if norm1 <= bt_const.SMALL_BUFFER or norm2 <= bt_const.SMALL_BUFFER:
|
|
31
|
+
return 180.0
|
|
32
|
+
|
|
33
|
+
dot = v1x * v2x + v1y * v2y
|
|
34
|
+
cos_val = max(-1.0, min(1.0, dot / (norm1 * norm2)))
|
|
35
|
+
return math.degrees(math.acos(cos_val))
|
|
36
|
+
|
|
37
|
+
def _shape_loss(self, work_coords, remove_index):
|
|
38
|
+
prev_coord = work_coords[remove_index - 1] if remove_index - 1 >= 0 else None
|
|
39
|
+
curr_coord = work_coords[remove_index]
|
|
40
|
+
next_coord = work_coords[remove_index + 1] if remove_index + 1 < len(work_coords) else None
|
|
41
|
+
|
|
42
|
+
old_scores = []
|
|
43
|
+
if prev_coord is not None and remove_index - 2 >= 0:
|
|
44
|
+
old_scores.append(self._bend_score(work_coords[remove_index - 2], prev_coord, curr_coord))
|
|
45
|
+
if next_coord is not None and remove_index + 2 < len(work_coords):
|
|
46
|
+
old_scores.append(self._bend_score(curr_coord, next_coord, work_coords[remove_index + 2]))
|
|
47
|
+
if prev_coord is not None and next_coord is not None:
|
|
48
|
+
old_scores.append(self._bend_score(prev_coord, curr_coord, next_coord))
|
|
49
|
+
|
|
50
|
+
new_scores = []
|
|
51
|
+
if prev_coord is not None and next_coord is not None:
|
|
52
|
+
if remove_index - 2 >= 0:
|
|
53
|
+
new_scores.append(self._bend_score(work_coords[remove_index - 2], prev_coord, next_coord))
|
|
54
|
+
if remove_index + 2 < len(work_coords):
|
|
55
|
+
new_scores.append(self._bend_score(prev_coord, next_coord, work_coords[remove_index + 2]))
|
|
56
|
+
|
|
57
|
+
old_peak = max(old_scores) if old_scores else 0.0
|
|
58
|
+
new_peak = max(new_scores) if new_scores else 0.0
|
|
59
|
+
significant_penalty = 50.0 if old_peak > self.angle_tol and new_peak <= self.angle_tol else 0.0
|
|
60
|
+
return abs(old_peak - new_peak) + significant_penalty
|
|
61
|
+
|
|
62
|
+
def _dedupe_consecutive_coords(self, coords):
|
|
63
|
+
if not coords:
|
|
64
|
+
return []
|
|
65
|
+
|
|
66
|
+
work_coords = [coords[0]]
|
|
67
|
+
for coord in coords[1:]:
|
|
68
|
+
if self._point_distance(work_coords[-1], coord) > bt_const.SMALL_BUFFER:
|
|
69
|
+
work_coords.append(coord)
|
|
70
|
+
|
|
71
|
+
return work_coords
|
|
72
|
+
|
|
73
|
+
def _finalize_line(self, coords, original_line):
|
|
74
|
+
if len(coords) < 2:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
if self._point_distance(coords[0], coords[-1]) <= bt_const.SMALL_BUFFER:
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
if len(coords) == len(original_line.coords):
|
|
81
|
+
return original_line
|
|
82
|
+
|
|
83
|
+
return sh_geom.LineString(coords)
|
|
84
|
+
|
|
85
|
+
def _cleanup_endpoint_only(self, coords):
|
|
86
|
+
while len(coords) > 2 and self._point_distance(coords[0], coords[1]) < self.close_distance:
|
|
87
|
+
coords.pop(1)
|
|
88
|
+
|
|
89
|
+
while len(coords) > 2 and self._point_distance(coords[-2], coords[-1]) < self.close_distance:
|
|
90
|
+
coords.pop(-2)
|
|
91
|
+
|
|
92
|
+
return coords
|
|
93
|
+
|
|
94
|
+
def cleanup_line(self, line, mode="full"):
|
|
95
|
+
if mode not in self.VALID_MODES:
|
|
96
|
+
raise ValueError(f"Unsupported vertex cleanup mode: {mode}")
|
|
97
|
+
|
|
98
|
+
coords = [tuple(coord) for coord in line.coords]
|
|
99
|
+
if len(coords) <= 2:
|
|
100
|
+
return line
|
|
101
|
+
|
|
102
|
+
work_coords = self._dedupe_consecutive_coords(coords)
|
|
103
|
+
if len(work_coords) <= 2:
|
|
104
|
+
return self._finalize_line(work_coords, line)
|
|
105
|
+
|
|
106
|
+
work_coords = self._cleanup_endpoint_only(work_coords)
|
|
107
|
+
if mode == "endpoint_only":
|
|
108
|
+
return self._finalize_line(work_coords, line)
|
|
109
|
+
|
|
110
|
+
i = 1
|
|
111
|
+
while i < len(work_coords) - 2:
|
|
112
|
+
left_coord = work_coords[i]
|
|
113
|
+
right_coord = work_coords[i + 1]
|
|
114
|
+
if self._point_distance(left_coord, right_coord) >= self.close_distance:
|
|
115
|
+
i += 1
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
left_score = self._bend_score(work_coords[i - 1], left_coord, right_coord)
|
|
119
|
+
right_score = self._bend_score(left_coord, right_coord, work_coords[i + 2])
|
|
120
|
+
|
|
121
|
+
if left_score > self.angle_tol and right_score <= self.angle_tol:
|
|
122
|
+
remove_index = i + 1
|
|
123
|
+
elif right_score > self.angle_tol and left_score <= self.angle_tol:
|
|
124
|
+
remove_index = i
|
|
125
|
+
else:
|
|
126
|
+
remove_left_loss = self._shape_loss(work_coords, i)
|
|
127
|
+
remove_right_loss = self._shape_loss(work_coords, i + 1)
|
|
128
|
+
remove_index = i if remove_left_loss <= remove_right_loss else i + 1
|
|
129
|
+
|
|
130
|
+
work_coords.pop(remove_index)
|
|
131
|
+
if i > 1:
|
|
132
|
+
i -= 1
|
|
133
|
+
|
|
134
|
+
return self._finalize_line(work_coords, line)
|
|
135
|
+
|
|
136
|
+
def remove_close_vertices(self, gdf, mode="full"):
|
|
137
|
+
if mode not in self.VALID_MODES:
|
|
138
|
+
raise ValueError(f"Unsupported vertex cleanup mode: {mode}")
|
|
139
|
+
|
|
140
|
+
if gdf is None or gdf.empty:
|
|
141
|
+
return gdf
|
|
142
|
+
|
|
143
|
+
if isinstance(gdf, gpd.GeoSeries):
|
|
144
|
+
gdf = gpd.GeoDataFrame(geometry=gdf)
|
|
145
|
+
|
|
146
|
+
out_gdf = gdf.copy()
|
|
147
|
+
|
|
148
|
+
if any(getattr(geom, "geom_type", None) == "MultiLineString" for geom in out_gdf.geometry):
|
|
149
|
+
out_gdf = out_gdf.explode(index_parts=False)
|
|
150
|
+
|
|
151
|
+
cleaned_geoms = []
|
|
152
|
+
for geom in out_gdf.geometry:
|
|
153
|
+
if geom is None or geom.is_empty:
|
|
154
|
+
cleaned_geoms.append(None)
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
if geom.geom_type != "LineString":
|
|
158
|
+
cleaned_geoms.append(geom)
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
cleaned_geoms.append(self.cleanup_line(geom, mode=mode))
|
|
162
|
+
|
|
163
|
+
geom_series = gpd.GeoSeries(cleaned_geoms, index=out_gdf.index, crs=out_gdf.crs)
|
|
164
|
+
valid_mask = geom_series.notna() & ~geom_series.is_empty
|
|
165
|
+
out_gdf = out_gdf.loc[valid_mask].copy()
|
|
166
|
+
out_gdf.set_geometry(geom_series.loc[valid_mask], inplace=True)
|
|
167
|
+
out_gdf = out_gdf[out_gdf.geometry.length > bt_const.SMALL_BUFFER]
|
|
168
|
+
out_gdf = gpd.GeoDataFrame(out_gdf, crs=gdf.crs)
|
|
169
|
+
out_gdf.reset_index(drop=True, inplace=True)
|
|
170
|
+
|
|
171
|
+
return out_gdf
|
|
172
|
+
|