BERATools 0.2.0__py3-none-any.whl → 0.2.1__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.
Files changed (153) hide show
  1. beratools/__init__.py +1 -7
  2. beratools/core/algo_centerline.py +491 -351
  3. beratools/core/algo_common.py +497 -0
  4. beratools/core/algo_cost.py +192 -0
  5. beratools/core/{dijkstra_algorithm.py → algo_dijkstra.py} +503 -460
  6. beratools/core/algo_footprint_rel.py +577 -0
  7. beratools/core/algo_line_grouping.py +944 -0
  8. beratools/core/algo_merge_lines.py +214 -0
  9. beratools/core/algo_split_with_lines.py +304 -0
  10. beratools/core/algo_tiler.py +428 -0
  11. beratools/core/algo_vertex_optimization.py +469 -0
  12. beratools/core/constants.py +52 -86
  13. beratools/core/logger.py +76 -85
  14. beratools/core/tool_base.py +196 -133
  15. beratools/gui/__init__.py +11 -15
  16. beratools/gui/{beratools.json → assets/beratools.json} +2185 -2300
  17. beratools/gui/batch_processing_dlg.py +513 -463
  18. beratools/gui/bt_data.py +481 -487
  19. beratools/gui/bt_gui_main.py +710 -691
  20. beratools/gui/main.py +26 -0
  21. beratools/gui/map_window.py +162 -146
  22. beratools/gui/tool_widgets.py +725 -493
  23. beratools/tools/Beratools_r_script.r +1120 -1120
  24. beratools/tools/Ht_metrics.py +116 -116
  25. beratools/tools/__init__.py +7 -7
  26. beratools/tools/batch_processing.py +136 -132
  27. beratools/tools/canopy_threshold_relative.py +672 -670
  28. beratools/tools/canopycostraster.py +222 -222
  29. beratools/tools/centerline.py +136 -176
  30. beratools/tools/common.py +857 -885
  31. beratools/tools/fl_regen_csf.py +428 -428
  32. beratools/tools/forest_line_attributes.py +408 -408
  33. beratools/tools/line_footprint_absolute.py +213 -363
  34. beratools/tools/line_footprint_fixed.py +436 -282
  35. beratools/tools/line_footprint_functions.py +733 -720
  36. beratools/tools/line_footprint_relative.py +73 -64
  37. beratools/tools/line_grouping.py +45 -0
  38. beratools/tools/ln_relative_metrics.py +615 -615
  39. beratools/tools/r_cal_lpi_elai.r +24 -24
  40. beratools/tools/r_generate_pd_focalraster.r +100 -100
  41. beratools/tools/r_interface.py +79 -79
  42. beratools/tools/r_point_density.r +8 -8
  43. beratools/tools/rpy_chm2trees.py +86 -86
  44. beratools/tools/rpy_dsm_chm_by.py +81 -81
  45. beratools/tools/rpy_dtm_by.py +63 -63
  46. beratools/tools/rpy_find_cellsize.py +43 -43
  47. beratools/tools/rpy_gnd_csf.py +74 -74
  48. beratools/tools/rpy_hummock_hollow.py +85 -85
  49. beratools/tools/rpy_hummock_hollow_raster.py +71 -71
  50. beratools/tools/rpy_las_info.py +51 -51
  51. beratools/tools/rpy_laz2las.py +40 -40
  52. beratools/tools/rpy_lpi_elai_lascat.py +466 -466
  53. beratools/tools/rpy_normalized_lidar_by.py +56 -56
  54. beratools/tools/rpy_percent_above_dbh.py +80 -80
  55. beratools/tools/rpy_points2trees.py +88 -88
  56. beratools/tools/rpy_vegcoverage.py +94 -94
  57. beratools/tools/tiler.py +48 -206
  58. beratools/tools/tool_template.py +69 -54
  59. beratools/tools/vertex_optimization.py +61 -620
  60. beratools/tools/zonal_threshold.py +144 -144
  61. beratools-0.2.1.dist-info/METADATA +109 -0
  62. beratools-0.2.1.dist-info/RECORD +74 -0
  63. {beratools-0.2.0.dist-info → beratools-0.2.1.dist-info}/WHEEL +1 -1
  64. {beratools-0.2.0.dist-info → beratools-0.2.1.dist-info}/licenses/LICENSE +22 -22
  65. beratools/gui/cli.py +0 -18
  66. beratools/gui/gui.json +0 -8
  67. beratools/gui_tk/ASCII Banners.txt +0 -248
  68. beratools/gui_tk/__init__.py +0 -20
  69. beratools/gui_tk/beratools_main.py +0 -515
  70. beratools/gui_tk/bt_widgets.py +0 -442
  71. beratools/gui_tk/cli.py +0 -18
  72. beratools/gui_tk/img/BERALogo.png +0 -0
  73. beratools/gui_tk/img/closed.gif +0 -0
  74. beratools/gui_tk/img/closed.png +0 -0
  75. beratools/gui_tk/img/open.gif +0 -0
  76. beratools/gui_tk/img/open.png +0 -0
  77. beratools/gui_tk/img/tool.gif +0 -0
  78. beratools/gui_tk/img/tool.png +0 -0
  79. beratools/gui_tk/main.py +0 -14
  80. beratools/gui_tk/map_window.py +0 -144
  81. beratools/gui_tk/runner.py +0 -1481
  82. beratools/gui_tk/tooltip.py +0 -55
  83. beratools/third_party/pyqtlet2/__init__.py +0 -9
  84. beratools/third_party/pyqtlet2/leaflet/__init__.py +0 -26
  85. beratools/third_party/pyqtlet2/leaflet/control/__init__.py +0 -6
  86. beratools/third_party/pyqtlet2/leaflet/control/control.py +0 -59
  87. beratools/third_party/pyqtlet2/leaflet/control/draw.py +0 -52
  88. beratools/third_party/pyqtlet2/leaflet/control/layers.py +0 -20
  89. beratools/third_party/pyqtlet2/leaflet/core/Parser.py +0 -24
  90. beratools/third_party/pyqtlet2/leaflet/core/__init__.py +0 -2
  91. beratools/third_party/pyqtlet2/leaflet/core/evented.py +0 -180
  92. beratools/third_party/pyqtlet2/leaflet/layer/__init__.py +0 -5
  93. beratools/third_party/pyqtlet2/leaflet/layer/featuregroup.py +0 -34
  94. beratools/third_party/pyqtlet2/leaflet/layer/icon/__init__.py +0 -1
  95. beratools/third_party/pyqtlet2/leaflet/layer/icon/icon.py +0 -30
  96. beratools/third_party/pyqtlet2/leaflet/layer/imageoverlay.py +0 -18
  97. beratools/third_party/pyqtlet2/leaflet/layer/layer.py +0 -105
  98. beratools/third_party/pyqtlet2/leaflet/layer/layergroup.py +0 -45
  99. beratools/third_party/pyqtlet2/leaflet/layer/marker/__init__.py +0 -1
  100. beratools/third_party/pyqtlet2/leaflet/layer/marker/marker.py +0 -91
  101. beratools/third_party/pyqtlet2/leaflet/layer/tile/__init__.py +0 -2
  102. beratools/third_party/pyqtlet2/leaflet/layer/tile/gridlayer.py +0 -4
  103. beratools/third_party/pyqtlet2/leaflet/layer/tile/tilelayer.py +0 -16
  104. beratools/third_party/pyqtlet2/leaflet/layer/vector/__init__.py +0 -5
  105. beratools/third_party/pyqtlet2/leaflet/layer/vector/circle.py +0 -15
  106. beratools/third_party/pyqtlet2/leaflet/layer/vector/circlemarker.py +0 -18
  107. beratools/third_party/pyqtlet2/leaflet/layer/vector/path.py +0 -5
  108. beratools/third_party/pyqtlet2/leaflet/layer/vector/polygon.py +0 -14
  109. beratools/third_party/pyqtlet2/leaflet/layer/vector/polyline.py +0 -18
  110. beratools/third_party/pyqtlet2/leaflet/layer/vector/rectangle.py +0 -14
  111. beratools/third_party/pyqtlet2/leaflet/map/__init__.py +0 -1
  112. beratools/third_party/pyqtlet2/leaflet/map/map.py +0 -220
  113. beratools/third_party/pyqtlet2/mapwidget.py +0 -45
  114. beratools/third_party/pyqtlet2/web/custom.js +0 -43
  115. beratools/third_party/pyqtlet2/web/map.html +0 -23
  116. beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/layers-2x.png +0 -0
  117. beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/layers.png +0 -0
  118. beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/marker-icon-2x.png +0 -0
  119. beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/marker-icon.png +0 -0
  120. beratools/third_party/pyqtlet2/web/modules/leaflet_193/images/marker-shadow.png +0 -0
  121. beratools/third_party/pyqtlet2/web/modules/leaflet_193/leaflet.css +0 -656
  122. beratools/third_party/pyqtlet2/web/modules/leaflet_193/leaflet.js +0 -6
  123. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.codeclimate.yml +0 -14
  124. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.editorconfig +0 -4
  125. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.gitattributes +0 -22
  126. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/.travis.yml +0 -43
  127. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/LICENSE +0 -20
  128. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/layers-2x.png +0 -0
  129. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/layers.png +0 -0
  130. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/marker-icon-2x.png +0 -0
  131. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/marker-icon.png +0 -0
  132. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/marker-shadow.png +0 -0
  133. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/spritesheet-2x.png +0 -0
  134. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/spritesheet.png +0 -0
  135. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/images/spritesheet.svg +0 -156
  136. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/leaflet.draw.css +0 -10
  137. beratools/third_party/pyqtlet2/web/modules/leaflet_draw_414/leaflet.draw.js +0 -10
  138. beratools/third_party/pyqtlet2/web/modules/leaflet_rotatedMarker_020/LICENSE +0 -22
  139. beratools/third_party/pyqtlet2/web/modules/leaflet_rotatedMarker_020/leaflet.rotatedMarker.js +0 -57
  140. beratools/tools/forest_line_ecosite.py +0 -216
  141. beratools/tools/lapis_all.py +0 -103
  142. beratools/tools/least_cost_path_from_chm.py +0 -152
  143. beratools-0.2.0.dist-info/METADATA +0 -63
  144. beratools-0.2.0.dist-info/RECORD +0 -142
  145. /beratools/gui/{img → assets}/BERALogo.png +0 -0
  146. /beratools/gui/{img → assets}/closed.gif +0 -0
  147. /beratools/gui/{img → assets}/closed.png +0 -0
  148. /beratools/{gui_tk → gui/assets}/gui.json +0 -0
  149. /beratools/gui/{img → assets}/open.gif +0 -0
  150. /beratools/gui/{img → assets}/open.png +0 -0
  151. /beratools/gui/{img → assets}/tool.gif +0 -0
  152. /beratools/gui/{img → assets}/tool.png +0 -0
  153. {beratools-0.2.0.dist-info → beratools-0.2.1.dist-info}/entry_points.txt +0 -0
@@ -1,460 +1,503 @@
1
- # -*- coding: utf-8 -*-
2
-
3
- """
4
- /***************************************************************************
5
- LeastCostPath Algorithm
6
- This algorithm is adapted from a QGIS plugin:
7
- Find the least cost path with given cost raster and points
8
- Original author: FlowMap Group@SESS.PKU
9
- Source code repository: https://github.com/Gooong/LeastCostPath
10
- ***************************************************************************/
11
-
12
- /***************************************************************************
13
- * *
14
- * This program is free software; you can redistribute it and/or modify *
15
- * it under the terms of the GNU General Public License as published by *
16
- * the Free Software Foundation; either version 2 of the License, or *
17
- * (at your option) any later version. *
18
- * *
19
- ***************************************************************************/
20
- """
21
-
22
- __author__ = 'Richard Zeng'
23
- __date__ = '2023-03-01'
24
- __copyright__ = '(C) 2023 by AppliedGRG'
25
-
26
- # This will get replaced with a git SHA1 when you do a git archive
27
- __revision__ = '$Format:%H$'
28
-
29
- from math import sqrt
30
- import queue
31
- from collections import defaultdict
32
- from skimage.graph import route_through_array
33
- from beratools.tools.common import *
34
-
35
- sqrt2 = sqrt(2)
36
- USE_NUMPY_FOR_DIJKSTRA = True
37
-
38
-
39
- class MinCostPathHelper:
40
- @staticmethod
41
- def _point_to_row_col(pointxy, ras_transform):
42
- col, row = ras_transform.rowcol(pointxy.x(), pointxy.y())
43
-
44
- return row, col
45
-
46
- @staticmethod
47
- def _row_col_to_point(row_col, ras_transform):
48
- x, y = ras_transform.xy(row_col[0], row_col[1])
49
- return x, y
50
-
51
- @staticmethod
52
- def create_points_from_path(ras_transform, min_cost_path, start_point, end_point):
53
- path_points = list(map(lambda row_col: MinCostPathHelper._row_col_to_point(row_col, ras_transform),
54
- min_cost_path))
55
- path_points[0] = (start_point.x, start_point.y)
56
- path_points[-1] = (end_point.x, end_point.y)
57
- return path_points
58
-
59
- @staticmethod
60
- def create_path_feature_from_points(path_points, attr_vals):
61
- path_points_raw = [[pt.x, pt.y] for pt in path_points]
62
-
63
- return LineString(path_points_raw), attr_vals
64
-
65
- @staticmethod
66
- def block2matrix_numpy(block, nodata):
67
- contains_negative = False
68
- with np.nditer(block, flags=["refs_ok"], op_flags=['readwrite']) as it:
69
- for x in it:
70
- # TODO: this speeds up a lot, but need further inspection
71
- # if np.isclose(x, nodata) or np.isnan(x):
72
- if x <= nodata or np.isnan(x):
73
- x[...] = 9999.0
74
- elif x < 0:
75
- contains_negative = True
76
-
77
- return block, contains_negative
78
-
79
- @staticmethod
80
- def block2matrix(block, nodata):
81
- contains_negative = False
82
- width, height = block.shape
83
- # TODO: deal with nodata
84
- matrix = [[None if np.isclose(block[i][j], nodata) or np.isclose(block[i][j], BT_NODATA)
85
- else block[i][j] for j in range(height)] for i in range(width)]
86
-
87
- for l in matrix:
88
- for v in l:
89
- if v is not None:
90
- if v < 0 and not np.isclose(v, BT_NODATA):
91
- contains_negative = True
92
-
93
- return matrix, contains_negative
94
-
95
-
96
- def dijkstra(start_tuple, end_tuples, block, find_nearest, feedback=None):
97
- class Grid:
98
- def __init__(self, matrix):
99
- self.map = matrix
100
- self.h = len(matrix)
101
- self.w = len(matrix[0])
102
- self.manhattan_boundry = None
103
- self.curr_boundry = None
104
-
105
- def _in_bounds(self, id):
106
- x, y = id
107
- return 0 <= x < self.h and 0 <= y < self.w
108
-
109
- def _passable(self, id):
110
- x, y = id
111
- return self.map[x][y] is not None
112
-
113
- def is_valid(self, id):
114
- return self._in_bounds(id) and self._passable(id)
115
-
116
- def neighbors(self, id):
117
- x, y = id
118
- results = [(x + 1, y), (x, y - 1), (x - 1, y), (x, y + 1),
119
- (x + 1, y - 1), (x + 1, y + 1), (x - 1, y - 1), (x - 1, y + 1)]
120
- results = list(filter(self.is_valid, results))
121
- return results
122
-
123
- @staticmethod
124
- def manhattan_distance(id1, id2):
125
- x1, y1 = id1
126
- x2, y2 = id2
127
- return abs(x1 - x2) + abs(y1 - y2)
128
-
129
- @staticmethod
130
- def min_manhattan(curr_node, end_nodes):
131
- return min(map(lambda node: Grid.manhattan_distance(curr_node, node), end_nodes))
132
-
133
- @staticmethod
134
- def max_manhattan(curr_node, end_nodes):
135
- return max(map(lambda node: Grid.manhattan_distance(curr_node, node), end_nodes))
136
-
137
- @staticmethod
138
- def all_manhattan(curr_node, end_nodes):
139
- return {end_node: Grid.manhattan_distance(curr_node, end_node) for end_node in end_nodes}
140
-
141
- def simple_cost(self, cur, nex):
142
- cx, cy = cur
143
- nx, ny = nex
144
- currV = self.map[cx][cy]
145
- offsetV = self.map[nx][ny]
146
- if cx == nx or cy == ny:
147
- return (currV + offsetV) / 2
148
- else:
149
- return sqrt2 * (currV + offsetV) / 2
150
-
151
- result = []
152
- grid = Grid(block)
153
-
154
- end_dict = defaultdict(list)
155
- for end_tuple in end_tuples:
156
- end_dict[end_tuple[0]].append(end_tuple)
157
- end_row_cols = set(end_dict.keys())
158
- end_row_col_list = list(end_row_cols)
159
- start_row_col = start_tuple[0]
160
-
161
- frontier = queue.PriorityQueue()
162
- frontier.put((0, start_row_col))
163
- came_from = {}
164
- cost_so_far = {}
165
- decided = set()
166
-
167
- if not grid.is_valid(start_row_col):
168
- return result
169
-
170
- # init progress
171
- index = 0
172
- distance_dic = grid.all_manhattan(start_row_col, end_row_cols)
173
- if find_nearest:
174
- total_manhattan = min(distance_dic.values())
175
- else:
176
- total_manhattan = sum(distance_dic.values())
177
-
178
- total_manhattan = total_manhattan + 1
179
- bound = total_manhattan
180
- if feedback:
181
- feedback.setProgress(1 + 100 * (1 - bound / total_manhattan))
182
-
183
- came_from[start_row_col] = None
184
- cost_so_far[start_row_col] = 0
185
-
186
- while not frontier.empty():
187
- _, current_node = frontier.get()
188
- if current_node in decided:
189
- continue
190
- decided.add(current_node)
191
-
192
- # update the progress bar
193
- if feedback:
194
- if feedback.isCanceled():
195
- return None
196
-
197
- index = (index + 1) % len(end_row_col_list)
198
- target_node = end_row_col_list[index]
199
- new_manhattan = grid.manhattan_distance(current_node, target_node)
200
- if new_manhattan < distance_dic[target_node]:
201
- if find_nearest:
202
- curr_bound = new_manhattan
203
- else:
204
- curr_bound = bound - (distance_dic[target_node] - new_manhattan)
205
-
206
- distance_dic[target_node] = new_manhattan
207
-
208
- if curr_bound < bound:
209
- bound = curr_bound
210
- if feedback:
211
- feedback.setProgress(1 + 100 * (1 - bound / total_manhattan) * (1 - bound / total_manhattan))
212
-
213
- # reacn destination
214
- if current_node in end_row_cols:
215
- path = []
216
- costs = []
217
- traverse_node = current_node
218
- while traverse_node is not None:
219
- path.append(traverse_node)
220
- costs.append(cost_so_far[traverse_node])
221
- traverse_node = came_from[traverse_node]
222
-
223
- # start point and end point overlaps
224
- if len(path) == 1:
225
- path.append(start_row_col)
226
- costs.append(0.0)
227
- path.reverse()
228
- costs.reverse()
229
- result.append((path, costs, end_dict[current_node]))
230
-
231
- end_row_cols.remove(current_node)
232
- end_row_col_list.remove(current_node)
233
- if len(end_row_cols) == 0 or find_nearest:
234
- break
235
-
236
- # relax distance
237
- for nex in grid.neighbors(current_node):
238
- new_cost = cost_so_far[current_node] + grid.simple_cost(current_node, nex)
239
- if nex not in cost_so_far or new_cost < cost_so_far[nex]:
240
- cost_so_far[nex] = new_cost
241
- frontier.put((new_cost, nex))
242
- came_from[nex] = current_node
243
-
244
- return result
245
-
246
-
247
- def valid_node(node, size_of_grid):
248
- """Checks if node is within the grid boundaries."""
249
- if node[0] < 0 or node[0] >= size_of_grid:
250
- return False
251
- if node[1] < 0 or node[1] >= size_of_grid:
252
- return False
253
- return True
254
-
255
-
256
- def up(node):
257
- return node[0] - 1, node[1]
258
-
259
-
260
- def down(node):
261
- return node[0] + 1, node[1]
262
-
263
-
264
- def left(node):
265
- return node[0], node[1] - 1
266
-
267
-
268
- def right(node):
269
- return node[0], node[1] + 1
270
-
271
-
272
- def backtrack(initial_node, desired_node, distances):
273
- # idea start at the last node then choose the least number of steps to go back
274
- # last node
275
- path = [desired_node]
276
-
277
- size_of_grid = distances.shape[0]
278
-
279
- while True:
280
- # check up down left right - choose the direction that has the least distance
281
- potential_distances = []
282
- potential_nodes = []
283
-
284
- directions = [up, down, left, right]
285
-
286
- for direction in directions:
287
- node = direction(path[-1])
288
- if valid_node(node, size_of_grid):
289
- potential_nodes.append(node)
290
- potential_distances.append(distances[node[0], node[1]])
291
-
292
- # least_distance_index = np.argmin(potential_distances)
293
- print(potential_nodes)
294
-
295
- least_distance_index = np.argsort(potential_distances)
296
-
297
- pt_added = False
298
- for index in least_distance_index:
299
- p_point = potential_nodes[index]
300
- if p_point == (1, 6):
301
- pass
302
- if p_point not in path:
303
- path.append(p_point)
304
- pt_added = True
305
- break
306
-
307
- if index >= len(potential_distances) - 1 and not pt_added:
308
- print("No best path found.")
309
- return
310
-
311
- if path[-1][0] == initial_node[0] and path[-1][1] == initial_node[1]:
312
- break
313
-
314
- return list(reversed(path))
315
-
316
-
317
- def dijkstra_np(start_tuple, end_tuple, matrix):
318
- """Dijkstras algorithm for finding the shortest path between two nodes in a graph.
319
-
320
- Args:
321
- start_node (list): [row,col] coordinates of the initial node
322
- end_node (list): [row,col] coordinates of the desired node
323
- matrix (array 2d): 2d numpy array that contains any matrix as 1s and free space as 0s
324
-
325
- Returns:
326
- list[list]: list of list of nodes that form the shortest path
327
- """
328
-
329
- # source and destination are free
330
- start_node = start_tuple[0]
331
- end_node = end_tuple[0]
332
- path = None
333
- costs = None
334
-
335
- try:
336
- matrix[start_node[0], start_node[1]] = 0
337
- matrix[end_node[0], end_node[1]] = 0
338
-
339
- path, cost = route_through_array(matrix, start_node, end_node)
340
- costs = [0.0 for i in range(len(path))]
341
- except Exception as e:
342
- print(e)
343
- return None
344
-
345
- return [(path, costs, end_tuple)]
346
-
347
-
348
- def find_least_cost_path(out_image, in_meta, line, find_nearest=True, output_linear_reference=False):
349
- default_return = None
350
- ras_nodata = in_meta['nodata']
351
-
352
- pt_start = line.coords[0]
353
- pt_end = line.coords[-1]
354
-
355
- out_image = np.where(out_image < 0, np.nan, out_image) # set negative value to nan
356
- if len(out_image.shape) > 2:
357
- out_image = np.squeeze(out_image, axis=0)
358
-
359
- if USE_NUMPY_FOR_DIJKSTRA:
360
- matrix, contains_negative = MinCostPathHelper.block2matrix_numpy(out_image, ras_nodata)
361
- else:
362
- matrix, contains_negative = MinCostPathHelper.block2matrix(out_image, ras_nodata)
363
-
364
- if contains_negative:
365
- print('ERROR: Raster has negative values.')
366
- return default_return
367
-
368
- transformer = rasterio.transform.AffineTransformer(in_meta['transform'])
369
-
370
- if (type(pt_start[0]) is tuple or
371
- type(pt_start[1]) is tuple or
372
- type(pt_end[0]) is tuple or
373
- type(pt_end[1]) is tuple):
374
- print("Point initialization error. Input is tuple.")
375
- return default_return
376
-
377
- start_tuples = []
378
- end_tuples = []
379
- start_tuple = []
380
- try:
381
- start_tuples = [(transformer.rowcol(pt_start[0], pt_start[1]), Point(pt_start[0], pt_start[1]), 0)]
382
- end_tuples = [(transformer.rowcol(pt_end[0], pt_end[1]), Point(pt_end[0], pt_end[1]), 1)]
383
- start_tuple = start_tuples[0]
384
- end_tuple = end_tuples[0]
385
-
386
- # regulate end poit coords in case they are out of index of matrix
387
- mat_size = matrix.shape
388
- mat_size = (mat_size[0] - 1, mat_size[0] - 1)
389
- start_tuple = (min(start_tuple[0], mat_size), start_tuple[1], start_tuple[2])
390
- end_tuple = (min(end_tuple[0], mat_size), end_tuple[1], end_tuple[2])
391
-
392
- except Exception as e:
393
- print(e)
394
-
395
- if USE_NUMPY_FOR_DIJKSTRA:
396
- result = dijkstra_np(start_tuple, end_tuple, matrix)
397
- else:
398
- # TODO: change end_tuples to end_tuple
399
- result = dijkstra(start_tuple, end_tuples, matrix, find_nearest)
400
-
401
- if result is None:
402
- # raise Exception
403
- return default_return
404
-
405
- if len(result) == 0:
406
- # raise Exception
407
- print('No result returned.')
408
- return default_return
409
-
410
- path_points = None
411
- for path, costs, end_tuple in result:
412
- path_points = MinCostPathHelper.create_points_from_path(transformer, path,
413
- start_tuple[1], end_tuple[1])
414
- if output_linear_reference:
415
- # TODO: code not reached
416
- # add linear reference
417
- for point, cost in zip(path_points, costs):
418
- point.addMValue(cost)
419
-
420
- total_cost = costs[-1]
421
-
422
- feat_attr = (start_tuple[2], end_tuple[2], total_cost)
423
- lc_path = None
424
- if len(path_points) >= 2:
425
- lc_path = LineString(path_points)
426
-
427
- return lc_path
428
-
429
-
430
- def find_least_cost_path_skimage(cost_clip, in_meta, seed_line):
431
- lc_path_new = []
432
- if len(cost_clip.shape) > 2:
433
- cost_clip = np.squeeze(cost_clip, axis=0)
434
-
435
- out_transform = in_meta['transform']
436
- transformer = rasterio.transform.AffineTransformer(out_transform)
437
-
438
- x1, y1 = list(seed_line.coords)[0][:2]
439
- x2, y2 = list(seed_line.coords)[-1][:2]
440
- row1, col1 = transformer.rowcol(x1, y1)
441
- row2, col2 = transformer.rowcol(x2, y2)
442
-
443
- try:
444
- path_new = route_through_array(cost_clip[0], [row1, col1], [row2, col2])
445
- except Exception as e:
446
- print(e)
447
- return None
448
-
449
- if path_new[0]:
450
- for row, col in path_new[0]:
451
- x, y = transformer.xy(row, col)
452
- lc_path_new.append((x, y))
453
-
454
- if len(lc_path_new) < 2:
455
- print('No least cost path detected, pass.')
456
- return None
457
- else:
458
- lc_path_new = LineString(lc_path_new)
459
-
460
- return lc_path_new
1
+ """
2
+ Least Cost Path Algorithm.
3
+
4
+ This algorithm is adapted from the QGIS plugin:
5
+ Find the least cost path with given cost raster and points
6
+ Original author: FlowMap Group@SESS.PKU
7
+ Source code repository: https://github.com/Gooong/LeastCostPath
8
+
9
+ Copyright (C) 2023 by AppliedGRG
10
+ Author: Richard Zeng
11
+ Date: 2023-03-01
12
+
13
+ This program is free software; you can redistribute it and/or modify
14
+ it under the terms of the GNU General Public License as published by
15
+ the Free Software Foundation; either version 2 of the License, or
16
+ (at your option) any later version.
17
+ """
18
+
19
+ # This will get replaced with a git SHA1 when you do a git archive
20
+ __revision__ = '$Format:%H$'
21
+
22
+ import math
23
+ import queue
24
+ from collections import defaultdict
25
+
26
+ import numpy as np
27
+ import rasterio
28
+ import shapely.geometry as sh_geom
29
+ import skimage.graph as sk_graph
30
+
31
+ import beratools.core.constants as bt_const
32
+
33
+ sqrt2 = math.sqrt(2)
34
+ USE_NUMPY_FOR_DIJKSTRA = True
35
+
36
+
37
+ class MinCostPathHelper:
38
+ """Helper class for the cost matrix."""
39
+
40
+ @staticmethod
41
+ def _point_to_row_col(point_xy, ras_transform):
42
+ col, row = ras_transform.rowcol(point_xy.x(), point_xy.y())
43
+
44
+ return row, col
45
+
46
+ @staticmethod
47
+ def _row_col_to_point(row_col, ras_transform):
48
+ x, y = ras_transform.xy(row_col[0], row_col[1])
49
+ return x, y
50
+
51
+ @staticmethod
52
+ def create_points_from_path(ras_transform, min_cost_path, start_point, end_point):
53
+ path_points = list(
54
+ map(
55
+ lambda row_col: MinCostPathHelper._row_col_to_point(
56
+ row_col, ras_transform
57
+ ),
58
+ min_cost_path,
59
+ )
60
+ )
61
+ path_points[0] = (start_point.x, start_point.y)
62
+ path_points[-1] = (end_point.x, end_point.y)
63
+ return path_points
64
+
65
+ @staticmethod
66
+ def create_path_feature_from_points(path_points, attr_vals):
67
+ path_points_raw = [[pt.x, pt.y] for pt in path_points]
68
+
69
+ return sh_geom.LineString(path_points_raw), attr_vals
70
+
71
+ @staticmethod
72
+ def block2matrix_numpy(block, nodata):
73
+ contains_negative = False
74
+ with np.nditer(block, flags=["refs_ok"], op_flags=['readwrite']) as it:
75
+ for x in it:
76
+ # TODO: this speeds up a lot, but need further inspection
77
+ # if np.isclose(x, nodata) or np.isnan(x):
78
+ if x <= nodata or np.isnan(x):
79
+ x[...] = 9999.0
80
+ elif x < 0:
81
+ contains_negative = True
82
+
83
+ return block, contains_negative
84
+
85
+ @staticmethod
86
+ def block2matrix(block, nodata):
87
+ contains_negative = False
88
+ width, height = block.shape
89
+ # TODO: deal with nodata
90
+ matrix = [
91
+ [
92
+ None
93
+ if np.isclose(block[i][j], nodata)
94
+ or np.isclose(block[i][j], bt_const.BT_NODATA)
95
+ else block[i][j]
96
+ for j in range(height)
97
+ ]
98
+ for i in range(width)
99
+ ]
100
+
101
+ for row in matrix:
102
+ for v in row:
103
+ if v is not None:
104
+ if v < 0 and not np.isclose(v, bt_const.BT_NODATA):
105
+ contains_negative = True
106
+
107
+ return matrix, contains_negative
108
+
109
+
110
+ def dijkstra(start_tuple, end_tuples, block, find_nearest, feedback=None):
111
+ class Grid:
112
+ def __init__(self, matrix):
113
+ self.map = matrix
114
+ self.h = len(matrix)
115
+ self.w = len(matrix[0])
116
+ self.manhattan_boundary = None
117
+ self.curr_boundary = None
118
+
119
+ def _in_bounds(self, id):
120
+ x, y = id
121
+ return 0 <= x < self.h and 0 <= y < self.w
122
+
123
+ def _passable(self, id):
124
+ x, y = id
125
+ return self.map[x][y] is not None
126
+
127
+ def is_valid(self, id):
128
+ return self._in_bounds(id) and self._passable(id)
129
+
130
+ def neighbors(self, id):
131
+ x, y = id
132
+ results = [(x + 1, y), (x, y - 1), (x - 1, y), (x, y + 1),
133
+ (x + 1, y - 1), (x + 1, y + 1), (x - 1, y - 1), (x - 1, y + 1)]
134
+ results = list(filter(self.is_valid, results))
135
+ return results
136
+
137
+ @staticmethod
138
+ def manhattan_distance(id1, id2):
139
+ x1, y1 = id1
140
+ x2, y2 = id2
141
+ return abs(x1 - x2) + abs(y1 - y2)
142
+
143
+ @staticmethod
144
+ def min_manhattan(curr_node, end_nodes):
145
+ return min(
146
+ map(lambda node: Grid.manhattan_distance(curr_node, node), end_nodes)
147
+ )
148
+
149
+ @staticmethod
150
+ def max_manhattan(curr_node, end_nodes):
151
+ return max(
152
+ map(lambda node: Grid.manhattan_distance(curr_node, node), end_nodes)
153
+ )
154
+
155
+ @staticmethod
156
+ def all_manhattan(curr_node, end_nodes):
157
+ return {
158
+ end_node: Grid.manhattan_distance(curr_node, end_node)
159
+ for end_node in end_nodes
160
+ }
161
+
162
+ def simple_cost(self, cur, nex):
163
+ cx, cy = cur
164
+ nx, ny = nex
165
+ currV = self.map[cx][cy]
166
+ offsetV = self.map[nx][ny]
167
+ if cx == nx or cy == ny:
168
+ return (currV + offsetV) / 2
169
+ else:
170
+ return sqrt2 * (currV + offsetV) / 2
171
+
172
+ result = []
173
+ grid = Grid(block)
174
+
175
+ end_dict = defaultdict(list)
176
+ for end_tuple in end_tuples:
177
+ end_dict[end_tuple[0]].append(end_tuple)
178
+ end_row_cols = set(end_dict.keys())
179
+ end_row_col_list = list(end_row_cols)
180
+ start_row_col = start_tuple[0]
181
+
182
+ frontier = queue.PriorityQueue()
183
+ frontier.put((0, start_row_col))
184
+ came_from = {}
185
+ cost_so_far = {}
186
+ decided = set()
187
+
188
+ if not grid.is_valid(start_row_col):
189
+ return result
190
+
191
+ # init progress
192
+ index = 0
193
+ distance_dic = grid.all_manhattan(start_row_col, end_row_cols)
194
+ if find_nearest:
195
+ total_manhattan = min(distance_dic.values())
196
+ else:
197
+ total_manhattan = sum(distance_dic.values())
198
+
199
+ total_manhattan = total_manhattan + 1
200
+ bound = total_manhattan
201
+ if feedback:
202
+ feedback.setProgress(1 + 100 * (1 - bound / total_manhattan))
203
+
204
+ came_from[start_row_col] = None
205
+ cost_so_far[start_row_col] = 0
206
+
207
+ while not frontier.empty():
208
+ _, current_node = frontier.get()
209
+ if current_node in decided:
210
+ continue
211
+ decided.add(current_node)
212
+
213
+ # update the progress bar
214
+ if feedback:
215
+ if feedback.isCanceled():
216
+ return None
217
+
218
+ index = (index + 1) % len(end_row_col_list)
219
+ target_node = end_row_col_list[index]
220
+ new_manhattan = grid.manhattan_distance(current_node, target_node)
221
+ if new_manhattan < distance_dic[target_node]:
222
+ if find_nearest:
223
+ curr_bound = new_manhattan
224
+ else:
225
+ curr_bound = bound - (distance_dic[target_node] - new_manhattan)
226
+
227
+ distance_dic[target_node] = new_manhattan
228
+
229
+ if curr_bound < bound:
230
+ bound = curr_bound
231
+ if feedback:
232
+ feedback.setProgress(
233
+ 1
234
+ + 100
235
+ * (1 - bound / total_manhattan)
236
+ * (1 - bound / total_manhattan)
237
+ )
238
+
239
+ # destination
240
+ if current_node in end_row_cols:
241
+ path = []
242
+ costs = []
243
+ traverse_node = current_node
244
+ while traverse_node is not None:
245
+ path.append(traverse_node)
246
+ costs.append(cost_so_far[traverse_node])
247
+ traverse_node = came_from[traverse_node]
248
+
249
+ # start point and end point overlaps
250
+ if len(path) == 1:
251
+ path.append(start_row_col)
252
+ costs.append(0.0)
253
+ path.reverse()
254
+ costs.reverse()
255
+ result.append((path, costs, end_dict[current_node]))
256
+
257
+ end_row_cols.remove(current_node)
258
+ end_row_col_list.remove(current_node)
259
+ if len(end_row_cols) == 0 or find_nearest:
260
+ break
261
+
262
+ # relax distance
263
+ for nex in grid.neighbors(current_node):
264
+ new_cost = cost_so_far[current_node] + grid.simple_cost(current_node, nex)
265
+ if nex not in cost_so_far or new_cost < cost_so_far[nex]:
266
+ cost_so_far[nex] = new_cost
267
+ frontier.put((new_cost, nex))
268
+ came_from[nex] = current_node
269
+
270
+ return result
271
+
272
+
273
+ def valid_node(node, size_of_grid):
274
+ """Check if node is within the grid boundaries."""
275
+ if node[0] < 0 or node[0] >= size_of_grid:
276
+ return False
277
+ if node[1] < 0 or node[1] >= size_of_grid:
278
+ return False
279
+ return True
280
+
281
+
282
+ def up(node):
283
+ return node[0] - 1, node[1]
284
+
285
+
286
+ def down(node):
287
+ return node[0] + 1, node[1]
288
+
289
+
290
+ def left(node):
291
+ return node[0], node[1] - 1
292
+
293
+
294
+ def right(node):
295
+ return node[0], node[1] + 1
296
+
297
+
298
+ def backtrack(initial_node, desired_node, distances):
299
+ # idea start at the last node then choose the least number of steps to go back
300
+ # last node
301
+ path = [desired_node]
302
+
303
+ size_of_grid = distances.shape[0]
304
+
305
+ while True:
306
+ # check up down left right - choose the direction that has the least distance
307
+ potential_distances = []
308
+ potential_nodes = []
309
+
310
+ directions = [up, down, left, right]
311
+
312
+ for direction in directions:
313
+ node = direction(path[-1])
314
+ if valid_node(node, size_of_grid):
315
+ potential_nodes.append(node)
316
+ potential_distances.append(distances[node[0], node[1]])
317
+
318
+ print(potential_nodes)
319
+
320
+ least_distance_index = np.argsort(potential_distances)
321
+
322
+ pt_added = False
323
+ for index in least_distance_index:
324
+ p_point = potential_nodes[index]
325
+ if p_point == (1, 6):
326
+ pass
327
+ if p_point not in path:
328
+ path.append(p_point)
329
+ pt_added = True
330
+ break
331
+
332
+ if index >= len(potential_distances) - 1 and not pt_added:
333
+ print("No best path found.")
334
+ return
335
+
336
+ if path[-1][0] == initial_node[0] and path[-1][1] == initial_node[1]:
337
+ break
338
+
339
+ return list(reversed(path))
340
+
341
+
342
+ def dijkstra_np(start_tuple, end_tuple, matrix):
343
+ """
344
+ Dijkstra's algorithm for finding the shortest path between two nodes in a graph.
345
+
346
+ Args:
347
+ start_node (list): [row,col] coordinates of the initial node
348
+ end_node (list): [row,col] coordinates of the desired node
349
+ matrix (array 2d): numpy array that contains matrix as 1s and free space as 0s
350
+
351
+ Returns:
352
+ list[list]: list of list of nodes that form the shortest path
353
+
354
+ """
355
+ # source and destination are free
356
+ start_node = start_tuple[0]
357
+ end_node = end_tuple[0]
358
+ path = None
359
+ costs = None
360
+
361
+ try:
362
+ matrix[start_node[0], start_node[1]] = 0
363
+ matrix[end_node[0], end_node[1]] = 0
364
+
365
+ path, cost = sk_graph.route_through_array(matrix, start_node, end_node)
366
+ costs = [0.0 for i in range(len(path))]
367
+ except Exception as e:
368
+ print(f"dijkstra_np: {e}")
369
+ return None
370
+
371
+ return [(path, costs, end_tuple)]
372
+
373
+
374
+ def find_least_cost_path(
375
+ out_image, in_meta, line, find_nearest=True, output_linear_reference=False
376
+ ):
377
+ default_return = None
378
+ ras_nodata = in_meta['nodata']
379
+
380
+ pt_start = line.coords[0]
381
+ pt_end = line.coords[-1]
382
+
383
+ out_image = np.where(out_image < 0, np.nan, out_image) # set negative value to nan
384
+ if len(out_image.shape) > 2:
385
+ out_image = np.squeeze(out_image, axis=0)
386
+
387
+ if USE_NUMPY_FOR_DIJKSTRA:
388
+ matrix, contains_negative = MinCostPathHelper.block2matrix_numpy(
389
+ out_image, ras_nodata
390
+ )
391
+ else:
392
+ matrix, contains_negative = MinCostPathHelper.block2matrix(
393
+ out_image, ras_nodata
394
+ )
395
+
396
+ if contains_negative:
397
+ print('ERROR: Raster has negative values.')
398
+ return default_return
399
+
400
+ transformer = rasterio.transform.AffineTransformer(in_meta['transform'])
401
+
402
+ if (type(pt_start[0]) is tuple or
403
+ type(pt_start[1]) is tuple or
404
+ type(pt_end[0]) is tuple or
405
+ type(pt_end[1]) is tuple):
406
+ print("Point initialization error. Input is tuple.")
407
+ return default_return
408
+
409
+ start_tuples = []
410
+ end_tuples = []
411
+ start_tuple = []
412
+ try:
413
+ start_tuples = [
414
+ (
415
+ transformer.rowcol(pt_start[0], pt_start[1]),
416
+ sh_geom.Point(pt_start[0], pt_start[1]),
417
+ 0,
418
+ )
419
+ ]
420
+ end_tuples = [
421
+ (
422
+ transformer.rowcol(pt_end[0], pt_end[1]),
423
+ sh_geom.Point(pt_end[0], pt_end[1]),
424
+ 1,
425
+ )
426
+ ]
427
+ start_tuple = start_tuples[0]
428
+ end_tuple = end_tuples[0]
429
+
430
+ # regulate end point coords in case they are out of index of matrix
431
+ mat_size = matrix.shape
432
+ mat_size = (mat_size[0] - 1, mat_size[0] - 1)
433
+ start_tuple = (min(start_tuple[0], mat_size), start_tuple[1], start_tuple[2])
434
+ end_tuple = (min(end_tuple[0], mat_size), end_tuple[1], end_tuple[2])
435
+
436
+ except Exception as e:
437
+ print(f"find_least_cost_path: {e}")
438
+
439
+ if USE_NUMPY_FOR_DIJKSTRA:
440
+ result = dijkstra_np(start_tuple, end_tuple, matrix)
441
+ else:
442
+ # TODO: change end_tuples to end_tuple
443
+ result = dijkstra(start_tuple, end_tuples, matrix, find_nearest)
444
+
445
+ if result is None:
446
+ return default_return
447
+
448
+ if len(result) == 0:
449
+ print('No result returned.')
450
+ return default_return
451
+
452
+ path_points = None
453
+ for path, costs, end_tuple in result:
454
+ path_points = MinCostPathHelper.create_points_from_path(
455
+ transformer, path, start_tuple[1], end_tuple[1]
456
+ )
457
+ if output_linear_reference:
458
+ # TODO: code not reached
459
+ # add linear reference
460
+ for point, cost in zip(path_points, costs):
461
+ point.addMValue(cost)
462
+
463
+ # feat_attr = (start_tuple[2], end_tuple[2], total_cost)
464
+ lc_path = None
465
+ if len(path_points) >= 2:
466
+ lc_path = sh_geom.LineString(path_points)
467
+
468
+ return lc_path
469
+
470
+
471
+ def find_least_cost_path_skimage(cost_clip, in_meta, seed_line):
472
+ lc_path_new = []
473
+ if len(cost_clip.shape) > 2:
474
+ cost_clip = np.squeeze(cost_clip, axis=0)
475
+
476
+ out_transform = in_meta['transform']
477
+ transformer = rasterio.transform.AffineTransformer(out_transform)
478
+
479
+ x1, y1 = list(seed_line.coords)[0][:2]
480
+ x2, y2 = list(seed_line.coords)[-1][:2]
481
+ row1, col1 = transformer.rowcol(x1, y1)
482
+ row2, col2 = transformer.rowcol(x2, y2)
483
+
484
+ try:
485
+ path_new = sk_graph.route_through_array(
486
+ cost_clip[0], [row1, col1], [row2, col2]
487
+ )
488
+ except Exception as e:
489
+ print(f"find_least_cost_path_skimage: {e}")
490
+ return None
491
+
492
+ if path_new[0]:
493
+ for row, col in path_new[0]:
494
+ x, y = transformer.xy(row, col)
495
+ lc_path_new.append((x, y))
496
+
497
+ if len(lc_path_new) < 2:
498
+ print('No least cost path detected, pass.')
499
+ return None
500
+ else:
501
+ lc_path_new = sh_geom.LineString(lc_path_new)
502
+
503
+ return lc_path_new