BERATools 0.1.0__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 (44) hide show
  1. beratools/__init__.py +3 -0
  2. beratools/core/__init__.py +0 -0
  3. beratools/core/algo_centerline.py +476 -0
  4. beratools/core/algo_common.py +489 -0
  5. beratools/core/algo_cost.py +185 -0
  6. beratools/core/algo_dijkstra.py +492 -0
  7. beratools/core/algo_footprint_rel.py +693 -0
  8. beratools/core/algo_line_grouping.py +941 -0
  9. beratools/core/algo_merge_lines.py +255 -0
  10. beratools/core/algo_split_with_lines.py +296 -0
  11. beratools/core/algo_vertex_optimization.py +451 -0
  12. beratools/core/constants.py +56 -0
  13. beratools/core/logger.py +92 -0
  14. beratools/core/tool_base.py +126 -0
  15. beratools/gui/__init__.py +11 -0
  16. beratools/gui/assets/BERALogo.png +0 -0
  17. beratools/gui/assets/beratools.json +471 -0
  18. beratools/gui/assets/closed.gif +0 -0
  19. beratools/gui/assets/closed.png +0 -0
  20. beratools/gui/assets/gui.json +8 -0
  21. beratools/gui/assets/open.gif +0 -0
  22. beratools/gui/assets/open.png +0 -0
  23. beratools/gui/assets/tool.gif +0 -0
  24. beratools/gui/assets/tool.png +0 -0
  25. beratools/gui/bt_data.py +485 -0
  26. beratools/gui/bt_gui_main.py +700 -0
  27. beratools/gui/main.py +27 -0
  28. beratools/gui/tool_widgets.py +730 -0
  29. beratools/tools/__init__.py +7 -0
  30. beratools/tools/canopy_threshold_relative.py +769 -0
  31. beratools/tools/centerline.py +127 -0
  32. beratools/tools/check_seed_line.py +48 -0
  33. beratools/tools/common.py +622 -0
  34. beratools/tools/line_footprint_absolute.py +203 -0
  35. beratools/tools/line_footprint_fixed.py +480 -0
  36. beratools/tools/line_footprint_functions.py +884 -0
  37. beratools/tools/line_footprint_relative.py +75 -0
  38. beratools/tools/tool_template.py +72 -0
  39. beratools/tools/vertex_optimization.py +57 -0
  40. beratools-0.1.0.dist-info/METADATA +134 -0
  41. beratools-0.1.0.dist-info/RECORD +44 -0
  42. beratools-0.1.0.dist-info/WHEEL +4 -0
  43. beratools-0.1.0.dist-info/entry_points.txt +2 -0
  44. beratools-0.1.0.dist-info/licenses/LICENSE +22 -0
@@ -0,0 +1,492 @@
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(row_col, ras_transform),
56
+ min_cost_path,
57
+ )
58
+ )
59
+ path_points[0] = (start_point.x, start_point.y)
60
+ path_points[-1] = (end_point.x, end_point.y)
61
+ return path_points
62
+
63
+ @staticmethod
64
+ def create_path_feature_from_points(path_points, attr_vals):
65
+ path_points_raw = [[pt.x, pt.y] for pt in path_points]
66
+
67
+ return sh_geom.LineString(path_points_raw), attr_vals
68
+
69
+ @staticmethod
70
+ def block2matrix_numpy(block, nodata):
71
+ contains_negative = False
72
+ with np.nditer(block, flags=["refs_ok"], op_flags=["readwrite"]) as it:
73
+ for x in it:
74
+ # TODO: this speeds up a lot, but need further inspection
75
+ # if np.isclose(x, nodata) or np.isnan(x):
76
+ if x <= nodata or np.isnan(x):
77
+ x[...] = 9999.0
78
+ elif x < 0:
79
+ contains_negative = True
80
+
81
+ return block, contains_negative
82
+
83
+ @staticmethod
84
+ def block2matrix(block, nodata):
85
+ contains_negative = False
86
+ width, height = block.shape
87
+ # TODO: deal with nodata
88
+ matrix = [
89
+ [
90
+ None
91
+ if np.isclose(block[i][j], nodata) or np.isclose(block[i][j], bt_const.BT_NODATA)
92
+ else block[i][j]
93
+ for j in range(height)
94
+ ]
95
+ for i in range(width)
96
+ ]
97
+
98
+ for row in matrix:
99
+ for v in row:
100
+ if v is not None:
101
+ if v < 0 and not np.isclose(v, bt_const.BT_NODATA):
102
+ contains_negative = True
103
+
104
+ return matrix, contains_negative
105
+
106
+
107
+ def dijkstra(start_tuple, end_tuples, block, find_nearest, feedback=None):
108
+ class Grid:
109
+ def __init__(self, matrix):
110
+ self.map = matrix
111
+ self.h = len(matrix)
112
+ self.w = len(matrix[0])
113
+ self.manhattan_boundary = None
114
+ self.curr_boundary = None
115
+
116
+ def _in_bounds(self, id):
117
+ x, y = id
118
+ return 0 <= x < self.h and 0 <= y < self.w
119
+
120
+ def _passable(self, id):
121
+ x, y = id
122
+ return self.map[x][y] is not None
123
+
124
+ def is_valid(self, id):
125
+ return self._in_bounds(id) and self._passable(id)
126
+
127
+ def neighbors(self, id):
128
+ x, y = id
129
+ results = [
130
+ (x + 1, y),
131
+ (x, y - 1),
132
+ (x - 1, y),
133
+ (x, y + 1),
134
+ (x + 1, y - 1),
135
+ (x + 1, y + 1),
136
+ (x - 1, y - 1),
137
+ (x - 1, y + 1),
138
+ ]
139
+ results = list(filter(self.is_valid, results))
140
+ return results
141
+
142
+ @staticmethod
143
+ def manhattan_distance(id1, id2):
144
+ x1, y1 = id1
145
+ x2, y2 = id2
146
+ return abs(x1 - x2) + abs(y1 - y2)
147
+
148
+ @staticmethod
149
+ def min_manhattan(curr_node, end_nodes):
150
+ return min(map(lambda node: Grid.manhattan_distance(curr_node, node), end_nodes))
151
+
152
+ @staticmethod
153
+ def max_manhattan(curr_node, end_nodes):
154
+ return max(map(lambda node: Grid.manhattan_distance(curr_node, node), end_nodes))
155
+
156
+ @staticmethod
157
+ def all_manhattan(curr_node, end_nodes):
158
+ return {end_node: Grid.manhattan_distance(curr_node, end_node) for end_node in end_nodes}
159
+
160
+ def simple_cost(self, cur, nex):
161
+ cx, cy = cur
162
+ nx, ny = nex
163
+ currV = self.map[cx][cy]
164
+ offsetV = self.map[nx][ny]
165
+ if cx == nx or cy == ny:
166
+ return (currV + offsetV) / 2
167
+ else:
168
+ return sqrt2 * (currV + offsetV) / 2
169
+
170
+ result = []
171
+ grid = Grid(block)
172
+
173
+ end_dict = defaultdict(list)
174
+ for end_tuple in end_tuples:
175
+ end_dict[end_tuple[0]].append(end_tuple)
176
+ end_row_cols = set(end_dict.keys())
177
+ end_row_col_list = list(end_row_cols)
178
+ start_row_col = start_tuple[0]
179
+
180
+ frontier = queue.PriorityQueue()
181
+ frontier.put((0, start_row_col))
182
+ came_from = {}
183
+ cost_so_far = {}
184
+ decided = set()
185
+
186
+ if not grid.is_valid(start_row_col):
187
+ return result
188
+
189
+ # init progress
190
+ index = 0
191
+ distance_dic = grid.all_manhattan(start_row_col, end_row_cols)
192
+ if find_nearest:
193
+ total_manhattan = min(distance_dic.values())
194
+ else:
195
+ total_manhattan = sum(distance_dic.values())
196
+
197
+ total_manhattan = total_manhattan + 1
198
+ bound = total_manhattan
199
+ if feedback:
200
+ feedback.setProgress(1 + 100 * (1 - bound / total_manhattan))
201
+
202
+ came_from[start_row_col] = None
203
+ cost_so_far[start_row_col] = 0
204
+
205
+ while not frontier.empty():
206
+ _, current_node = frontier.get()
207
+ if current_node in decided:
208
+ continue
209
+ decided.add(current_node)
210
+
211
+ # update the progress bar
212
+ if feedback:
213
+ if feedback.isCanceled():
214
+ return None
215
+
216
+ index = (index + 1) % len(end_row_col_list)
217
+ target_node = end_row_col_list[index]
218
+ new_manhattan = grid.manhattan_distance(current_node, target_node)
219
+ if new_manhattan < distance_dic[target_node]:
220
+ if find_nearest:
221
+ curr_bound = new_manhattan
222
+ else:
223
+ curr_bound = bound - (distance_dic[target_node] - new_manhattan)
224
+
225
+ distance_dic[target_node] = new_manhattan
226
+
227
+ if curr_bound < bound:
228
+ bound = curr_bound
229
+ if feedback:
230
+ feedback.setProgress(
231
+ 1 + 100 * (1 - bound / total_manhattan) * (1 - bound / total_manhattan)
232
+ )
233
+
234
+ # destination
235
+ if current_node in end_row_cols:
236
+ path = []
237
+ costs = []
238
+ traverse_node = current_node
239
+ while traverse_node is not None:
240
+ path.append(traverse_node)
241
+ costs.append(cost_so_far[traverse_node])
242
+ traverse_node = came_from[traverse_node]
243
+
244
+ # start point and end point overlaps
245
+ if len(path) == 1:
246
+ path.append(start_row_col)
247
+ costs.append(0.0)
248
+ path.reverse()
249
+ costs.reverse()
250
+ result.append((path, costs, end_dict[current_node]))
251
+
252
+ end_row_cols.remove(current_node)
253
+ end_row_col_list.remove(current_node)
254
+ if len(end_row_cols) == 0 or find_nearest:
255
+ break
256
+
257
+ # relax distance
258
+ for nex in grid.neighbors(current_node):
259
+ new_cost = cost_so_far[current_node] + grid.simple_cost(current_node, nex)
260
+ if nex not in cost_so_far or new_cost < cost_so_far[nex]:
261
+ cost_so_far[nex] = new_cost
262
+ frontier.put((new_cost, nex))
263
+ came_from[nex] = current_node
264
+
265
+ return result
266
+
267
+
268
+ def valid_node(node, size_of_grid):
269
+ """Check if node is within the grid boundaries."""
270
+ if node[0] < 0 or node[0] >= size_of_grid:
271
+ return False
272
+ if node[1] < 0 or node[1] >= size_of_grid:
273
+ return False
274
+ return True
275
+
276
+
277
+ def up(node):
278
+ return node[0] - 1, node[1]
279
+
280
+
281
+ def down(node):
282
+ return node[0] + 1, node[1]
283
+
284
+
285
+ def left(node):
286
+ return node[0], node[1] - 1
287
+
288
+
289
+ def right(node):
290
+ return node[0], node[1] + 1
291
+
292
+
293
+ def backtrack(initial_node, desired_node, distances):
294
+ # idea start at the last node then choose the least number of steps to go back
295
+ # last node
296
+ path = [desired_node]
297
+
298
+ size_of_grid = distances.shape[0]
299
+
300
+ while True:
301
+ # check up down left right - choose the direction that has the least distance
302
+ potential_distances = []
303
+ potential_nodes = []
304
+
305
+ directions = [up, down, left, right]
306
+
307
+ for direction in directions:
308
+ node = direction(path[-1])
309
+ if valid_node(node, size_of_grid):
310
+ potential_nodes.append(node)
311
+ potential_distances.append(distances[node[0], node[1]])
312
+
313
+ print(potential_nodes)
314
+
315
+ least_distance_index = np.argsort(potential_distances)
316
+
317
+ pt_added = False
318
+ for index in least_distance_index:
319
+ p_point = potential_nodes[index]
320
+ if p_point == (1, 6):
321
+ pass
322
+ if p_point not in path:
323
+ path.append(p_point)
324
+ pt_added = True
325
+ break
326
+
327
+ if index >= len(potential_distances) - 1 and not pt_added:
328
+ print("No best path found.")
329
+ return
330
+
331
+ if path[-1][0] == initial_node[0] and path[-1][1] == initial_node[1]:
332
+ break
333
+
334
+ return list(reversed(path))
335
+
336
+
337
+ def dijkstra_np(start_tuple, end_tuple, matrix):
338
+ """
339
+ Dijkstra's algorithm for finding the shortest path between two nodes in a graph.
340
+
341
+ Args:
342
+ start_node (list): [row,col] coordinates of the initial node
343
+ end_node (list): [row,col] coordinates of the desired node
344
+ matrix (array 2d): numpy array that contains matrix as 1s and free space as 0s
345
+
346
+ Returns:
347
+ list[list]: list of list of nodes that form the shortest path
348
+
349
+ """
350
+ # source and destination are free
351
+ start_node = start_tuple[0]
352
+ end_node = end_tuple[0]
353
+ path = None
354
+ costs = None
355
+
356
+ try:
357
+ matrix[start_node[0], start_node[1]] = 0
358
+ matrix[end_node[0], end_node[1]] = 0
359
+
360
+ path, cost = sk_graph.route_through_array(matrix, start_node, end_node)
361
+ costs = [0.0 for i in range(len(path))]
362
+ except Exception as e:
363
+ print(f"dijkstra_np: {e}")
364
+ return None
365
+
366
+ return [(path, costs, end_tuple)]
367
+
368
+
369
+ def find_least_cost_path(out_image, in_meta, line, find_nearest=True, output_linear_reference=False):
370
+ default_return = None
371
+ ras_nodata = in_meta["nodata"]
372
+
373
+ pt_start = line.coords[0]
374
+ pt_end = line.coords[-1]
375
+
376
+ out_image = np.where(out_image < 0, np.nan, out_image) # set negative value to nan
377
+ if len(out_image.shape) > 2:
378
+ out_image = np.squeeze(out_image, axis=0)
379
+
380
+ if USE_NUMPY_FOR_DIJKSTRA:
381
+ matrix, contains_negative = MinCostPathHelper.block2matrix_numpy(out_image, ras_nodata)
382
+ else:
383
+ matrix, contains_negative = MinCostPathHelper.block2matrix(out_image, ras_nodata)
384
+
385
+ if contains_negative:
386
+ print("ERROR: Raster has negative values.")
387
+ return default_return
388
+
389
+ transformer = rasterio.transform.AffineTransformer(in_meta["transform"])
390
+
391
+ if (
392
+ type(pt_start[0]) is tuple
393
+ or type(pt_start[1]) is tuple
394
+ or type(pt_end[0]) is tuple
395
+ or type(pt_end[1]) is tuple
396
+ ):
397
+ print("Point initialization error. Input is tuple.")
398
+ return default_return
399
+
400
+ start_tuples = []
401
+ end_tuples = []
402
+ start_tuple = []
403
+ try:
404
+ start_tuples = [
405
+ (
406
+ transformer.rowcol(pt_start[0], pt_start[1]),
407
+ sh_geom.Point(pt_start[0], pt_start[1]),
408
+ 0,
409
+ )
410
+ ]
411
+ end_tuples = [
412
+ (
413
+ transformer.rowcol(pt_end[0], pt_end[1]),
414
+ sh_geom.Point(pt_end[0], pt_end[1]),
415
+ 1,
416
+ )
417
+ ]
418
+ start_tuple = start_tuples[0]
419
+ end_tuple = end_tuples[0]
420
+
421
+ # regulate end point coords in case they are out of index of matrix
422
+ mat_size = matrix.shape
423
+ mat_size = (mat_size[0] - 1, mat_size[0] - 1)
424
+ start_tuple = (min(start_tuple[0], mat_size), start_tuple[1], start_tuple[2])
425
+ end_tuple = (min(end_tuple[0], mat_size), end_tuple[1], end_tuple[2])
426
+
427
+ except Exception as e:
428
+ print(f"find_least_cost_path: {e}")
429
+
430
+ if USE_NUMPY_FOR_DIJKSTRA:
431
+ result = dijkstra_np(start_tuple, end_tuple, matrix)
432
+ else:
433
+ # TODO: change end_tuples to end_tuple
434
+ result = dijkstra(start_tuple, end_tuples, matrix, find_nearest)
435
+
436
+ if result is None:
437
+ return default_return
438
+
439
+ if len(result) == 0:
440
+ print("No result returned.")
441
+ return default_return
442
+
443
+ path_points = None
444
+ for path, costs, end_tuple in result:
445
+ path_points = MinCostPathHelper.create_points_from_path(
446
+ transformer, path, start_tuple[1], end_tuple[1]
447
+ )
448
+ if output_linear_reference:
449
+ # TODO: code not reached
450
+ # add linear reference
451
+ for point, cost in zip(path_points, costs):
452
+ point.addMValue(cost)
453
+
454
+ # feat_attr = (start_tuple[2], end_tuple[2], total_cost)
455
+ lc_path = None
456
+ if len(path_points) >= 2:
457
+ lc_path = sh_geom.LineString(path_points)
458
+
459
+ return lc_path
460
+
461
+
462
+ def find_least_cost_path_skimage(cost_clip, in_meta, seed_line):
463
+ lc_path_new = []
464
+ if len(cost_clip.shape) > 2:
465
+ cost_clip = np.squeeze(cost_clip, axis=0)
466
+
467
+ out_transform = in_meta["transform"]
468
+ transformer = rasterio.transform.AffineTransformer(out_transform)
469
+
470
+ x1, y1 = list(seed_line.coords)[0][:2]
471
+ x2, y2 = list(seed_line.coords)[-1][:2]
472
+ row1, col1 = transformer.rowcol(x1, y1)
473
+ row2, col2 = transformer.rowcol(x2, y2)
474
+
475
+ try:
476
+ path_new = sk_graph.route_through_array(cost_clip[0], [row1, col1], [row2, col2])
477
+ except Exception as e:
478
+ print(f"find_least_cost_path_skimage: {e}")
479
+ return None
480
+
481
+ if path_new[0]:
482
+ for row, col in path_new[0]:
483
+ x, y = transformer.xy(row, col)
484
+ lc_path_new.append((x, y))
485
+
486
+ if len(lc_path_new) < 2:
487
+ print("No least cost path detected, pass.")
488
+ return None
489
+ else:
490
+ lc_path_new = sh_geom.LineString(lc_path_new)
491
+
492
+ return lc_path_new