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,884 @@
1
+ # ---------------------------------------------------------------------------
2
+ # Copyright (C) 2021 Applied Geospatial Research Group
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, version 3.
7
+ #
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with this program. If not, see <https://gnu.org/licenses/gpl-3.0>.
15
+ #
16
+ # ---------------------------------------------------------------------------
17
+ #
18
+ # Refactor to use for produce dynamic footprint from dynamic canopy and cost raster with open source libraries
19
+ # Prerequisite: Line feature class must have the attribute Fields:"CorridorTh" "DynCanTh" "OLnFID"
20
+ # line_footprint_function.py
21
+ # Maverick Fong
22
+ # Date: 2023-Dec
23
+ # This script is part of the BERA toolset
24
+ # Webpage: https://github.com/
25
+ #
26
+ # Purpose: Creates dynamic footprint polygons for each input line based on a least
27
+ # cost corridor method and individual line thresholds.
28
+ #
29
+ # ---------------------------------------------------------------------------
30
+
31
+ import time
32
+ from geopandas import GeoDataFrame
33
+ from shapely import buffer
34
+ from rasterio import features
35
+ from rasterio.mask import mask
36
+ from xrspatial import convolution
37
+
38
+ from skimage.graph import MCP_Flexible
39
+
40
+ from shapely.geometry import shape
41
+ from shapely import LineString, Point, MultiPolygon
42
+
43
+ from beratools.core.constants import *
44
+ from beratools.core.algo_centerline import *
45
+ from beratools.tools.common import *
46
+ from beratools.core.tool_base import *
47
+
48
+
49
+ def dyn_canopy_cost_raster(args):
50
+ # raster_obj = args[0]
51
+ in_chm_raster = args[0]
52
+ DynCanTh = args[1]
53
+ tree_radius = args[2]
54
+ max_line_dist = args[3]
55
+ canopy_avoid = args[4]
56
+ exponent = args[5]
57
+ res = args[6]
58
+ nodata = args[7]
59
+ line_df = args[8]
60
+ out_meta = args[9]
61
+ line_id = args[10]
62
+ Cut_Dist = args[11]
63
+ Side = args[12]
64
+ canopy_thresh_percentage = float(args[13]) / 100
65
+ line_buffer = args[14]
66
+
67
+ if Side == "Left":
68
+ canopy_ht_threshold = line_df.CL_CutHt * canopy_thresh_percentage
69
+ elif Side == "Right":
70
+ canopy_ht_threshold = line_df.CR_CutHt * canopy_thresh_percentage
71
+ elif Side == "Center":
72
+ canopy_ht_threshold = DynCanTh * canopy_thresh_percentage
73
+ else:
74
+ canopy_ht_threshold = 0.5
75
+
76
+ canopy_ht_threshold = float(canopy_ht_threshold)
77
+ if canopy_ht_threshold <= 0:
78
+ canopy_ht_threshold = 0.5
79
+ tree_radius = float(tree_radius) # get the round up integer number for tree search radius
80
+ max_line_dist = float(max_line_dist)
81
+ canopy_avoid = float(canopy_avoid)
82
+ cost_raster_exponent = float(exponent)
83
+
84
+ try:
85
+ clipped_rasterC, out_meta = clip_raster(in_chm_raster, line_buffer, 0)
86
+
87
+ # clipped_rasterC, out_transformC = mask(raster_obj, [line_buffer], crop=True,
88
+ # filled=False)
89
+
90
+ in_chm = np.squeeze(clipped_rasterC, axis=0)
91
+
92
+ # # make rasterio meta for saving raster later
93
+ # out_meta = raster_obj.meta.copy()
94
+ # out_meta.update({"driver": "GTiff",
95
+ # "height": in_chm.shape[0],
96
+ # "width": in_chm.shape[1],
97
+ # "nodata": BT_NODATA,
98
+ # "transform": out_transformC})
99
+
100
+ # print('Loading CHM ...')
101
+ cell_x, cell_y = out_meta["transform"][0], -out_meta["transform"][4]
102
+
103
+ # print('Preparing Kernel window ...')
104
+ kernel = convolution.circle_kernel(cell_x, cell_y, int(tree_radius))
105
+
106
+ # Generate Canopy Raster and return the Canopy array
107
+ dyn_canopy_ndarray = dyn_np_cc_map(in_chm, canopy_ht_threshold, BT_NODATA)
108
+
109
+ # Calculating focal statistic from canopy raster
110
+ cc_std, cc_mean = dyn_fs_raster_stdmean(dyn_canopy_ndarray, kernel, BT_NODATA)
111
+
112
+ # Smoothing raster
113
+ cc_smooth = dyn_smooth_cost(dyn_canopy_ndarray, max_line_dist, [cell_x, cell_y])
114
+ avoidance = max(min(float(canopy_avoid), 1), 0)
115
+ cost_clip = dyn_np_cost_raster(
116
+ dyn_canopy_ndarray,
117
+ cc_mean,
118
+ cc_std,
119
+ cc_smooth,
120
+ avoidance,
121
+ cost_raster_exponent,
122
+ )
123
+ negative_cost_clip = np.where(
124
+ np.isnan(cost_clip), -9999, cost_clip
125
+ ) # TODO was nodata, changed to BT_NODATA_COST
126
+ return (
127
+ line_df,
128
+ dyn_canopy_ndarray,
129
+ negative_cost_clip,
130
+ out_meta,
131
+ max_line_dist,
132
+ nodata,
133
+ line_id,
134
+ Cut_Dist,
135
+ line_buffer,
136
+ )
137
+
138
+ except Exception as e:
139
+ print("Error in createing (dynamic) cost raster @ {}: {}".format(line_id, e))
140
+ return None
141
+
142
+
143
+ def split_line_fc(line):
144
+ return list(map(LineString, zip(line.coords[:-1], line.coords[1:])))
145
+
146
+
147
+ def split_line_npart(line):
148
+ # Work out n parts for each line (divided by LP_SEGMENT_LENGTH)
149
+ n = int(np.ceil(line.length / LP_SEGMENT_LENGTH))
150
+ if n > 1:
151
+ # divided line into n-1 equal parts;
152
+ distances = np.linspace(0, line.length, n)
153
+ points = [line.interpolate(distance) for distance in distances]
154
+ line = LineString(points)
155
+ mline = split_line_fc(line)
156
+ else:
157
+ mline = line
158
+ return mline
159
+
160
+
161
+ def split_into_segments(df):
162
+ odf = df
163
+ crs = odf.crs
164
+ if "OLnSEG" not in odf.columns.array:
165
+ df["OLnSEG"] = np.nan
166
+
167
+ df = odf.assign(geometry=odf.apply(lambda x: split_line_fc(x.geometry), axis=1))
168
+ df = df.explode()
169
+
170
+ df["OLnSEG"] = df.groupby("OLnFID").cumcount()
171
+ gdf = GeoDataFrame(df, geometry=df.geometry, crs=crs)
172
+ gdf = gdf.sort_values(by=["OLnFID", "OLnSEG"])
173
+ gdf = gdf.reset_index(drop=True)
174
+ return gdf
175
+
176
+
177
+ def split_into_equal_nth_segments(df):
178
+ odf = df
179
+ crs = odf.crs
180
+ if "OLnSEG" not in odf.columns.array:
181
+ df["OLnSEG"] = np.nan
182
+ df = odf.assign(geometry=odf.apply(lambda x: split_line_npart(x.geometry), axis=1))
183
+ df = df.explode(index_parts=True)
184
+
185
+ df["OLnSEG"] = df.groupby("OLnFID").cumcount()
186
+ gdf = GeoDataFrame(df, geometry=df.geometry, crs=crs)
187
+ gdf = gdf.sort_values(by=["OLnFID", "OLnSEG"])
188
+ gdf = gdf.reset_index(drop=True)
189
+ return gdf
190
+
191
+
192
+ def generate_line_args(
193
+ line_seg,
194
+ work_in_bufferL,
195
+ work_in_bufferC,
196
+ raster,
197
+ tree_radius,
198
+ max_line_dist,
199
+ canopy_avoidance,
200
+ exponent,
201
+ work_in_bufferR,
202
+ canopy_thresh_percentage,
203
+ ):
204
+ line_argsL = []
205
+ line_argsR = []
206
+ line_argsC = []
207
+ line_id = 0
208
+ for record in range(0, len(work_in_bufferL)):
209
+ line_bufferL = work_in_bufferL.loc[record, "geometry"]
210
+ line_bufferC = work_in_bufferC.loc[record, "geometry"]
211
+ LCut = work_in_bufferL.loc[record, "LDist_Cut"]
212
+
213
+ clipped_rasterL, out_transformL = mask(
214
+ raster, [line_bufferL], crop=True, nodata=BT_NODATA, filled=True
215
+ )
216
+ clipped_rasterL = np.squeeze(clipped_rasterL, axis=0)
217
+
218
+ clipped_rasterC, out_transformC = mask(
219
+ raster, [line_bufferC], crop=True, nodata=BT_NODATA, filled=True
220
+ )
221
+
222
+ clipped_rasterC = np.squeeze(clipped_rasterC, axis=0)
223
+
224
+ # make rasterio meta for saving raster later
225
+ out_metaL = raster.meta.copy()
226
+ out_metaL.update(
227
+ {
228
+ "driver": "GTiff",
229
+ "height": clipped_rasterL.shape[0],
230
+ "width": clipped_rasterL.shape[1],
231
+ "nodata": BT_NODATA,
232
+ "transform": out_transformL,
233
+ }
234
+ )
235
+
236
+ out_metaC = raster.meta.copy()
237
+ out_metaC.update(
238
+ {
239
+ "driver": "GTiff",
240
+ "height": clipped_rasterC.shape[0],
241
+ "width": clipped_rasterC.shape[1],
242
+ "nodata": BT_NODATA,
243
+ "transform": out_transformC,
244
+ }
245
+ )
246
+
247
+ nodata = BT_NODATA
248
+ line_argsL.append(
249
+ [
250
+ clipped_rasterL,
251
+ float(work_in_bufferL.loc[record, "DynCanTh"]),
252
+ float(tree_radius),
253
+ float(max_line_dist),
254
+ float(canopy_avoidance),
255
+ float(exponent),
256
+ raster.res,
257
+ nodata,
258
+ line_seg.iloc[[record]],
259
+ out_metaL,
260
+ line_id,
261
+ LCut,
262
+ "Left",
263
+ canopy_thresh_percentage,
264
+ line_bufferC,
265
+ ]
266
+ )
267
+
268
+ line_argsC.append(
269
+ [
270
+ clipped_rasterC,
271
+ float(work_in_bufferC.loc[record, "DynCanTh"]),
272
+ float(tree_radius),
273
+ float(max_line_dist),
274
+ float(canopy_avoidance),
275
+ float(exponent),
276
+ raster.res,
277
+ nodata,
278
+ line_seg.iloc[[record]],
279
+ out_metaC,
280
+ line_id,
281
+ 10,
282
+ "Center",
283
+ canopy_thresh_percentage,
284
+ line_bufferC,
285
+ ]
286
+ )
287
+
288
+ line_id += 1
289
+
290
+ line_id = 0
291
+ for record in range(0, len(work_in_bufferR)):
292
+ line_bufferR = work_in_bufferR.loc[record, "geometry"]
293
+ RCut = work_in_bufferR.loc[record, "RDist_Cut"]
294
+ clipped_rasterR, out_transformR = mask(
295
+ raster, [line_bufferR], crop=True, nodata=BT_NODATA, filled=True
296
+ )
297
+ clipped_rasterR = np.squeeze(clipped_rasterR, axis=0)
298
+
299
+ # make rasterio meta for saving raster later
300
+ out_metaR = raster.meta.copy()
301
+ out_metaR.update(
302
+ {
303
+ "driver": "GTiff",
304
+ "height": clipped_rasterR.shape[0],
305
+ "width": clipped_rasterR.shape[1],
306
+ "nodata": BT_NODATA,
307
+ "transform": out_transformR,
308
+ }
309
+ )
310
+ line_bufferC = work_in_bufferC.loc[record, "geometry"]
311
+ clipped_rasterC, out_transformC = mask(
312
+ raster, [line_bufferC], crop=True, nodata=BT_NODATA, filled=True
313
+ )
314
+
315
+ clipped_rasterC = np.squeeze(clipped_rasterC, axis=0)
316
+ out_metaC = raster.meta.copy()
317
+ out_metaC.update(
318
+ {
319
+ "driver": "GTiff",
320
+ "height": clipped_rasterC.shape[0],
321
+ "width": clipped_rasterC.shape[1],
322
+ "nodata": BT_NODATA,
323
+ "transform": out_transformC,
324
+ }
325
+ )
326
+
327
+ nodata = BT_NODATA
328
+ # TODO deal with inherited nodata and BT_NODATA_COST
329
+ # TODO convert nodata to BT_NODATA_COST
330
+ line_argsR.append(
331
+ [
332
+ clipped_rasterR,
333
+ float(work_in_bufferR.loc[record, "DynCanTh"]),
334
+ float(tree_radius),
335
+ float(max_line_dist),
336
+ float(canopy_avoidance),
337
+ float(exponent),
338
+ raster.res,
339
+ nodata,
340
+ line_seg.iloc[[record]],
341
+ out_metaR,
342
+ line_id,
343
+ RCut,
344
+ "Right",
345
+ canopy_thresh_percentage,
346
+ line_bufferC,
347
+ ]
348
+ )
349
+
350
+ print(
351
+ ' "PROGRESS_LABEL Preparing... {} of {}" '.format(
352
+ line_id + 1 + len(work_in_bufferL),
353
+ len(work_in_bufferL) + len(work_in_bufferR),
354
+ ),
355
+ flush=True,
356
+ )
357
+ print(
358
+ " %{} ".format(
359
+ (line_id + 1 + len(work_in_bufferL)) / (len(work_in_bufferL) + len(work_in_bufferR)) * 100
360
+ ),
361
+ flush=True,
362
+ )
363
+
364
+ line_id += 1
365
+
366
+ return line_argsL, line_argsR, line_argsC
367
+
368
+
369
+ #
370
+ # def find_corridor_threshold_boundary(canopy_clip, least_cost_path, corridor_raster):
371
+ # threshold = -1
372
+ # thresholds = [-1] * 10
373
+ #
374
+ # # morphological filters to get polygons from canopy raster
375
+ # canopy_bin = np.where(np.isclose(canopy_clip, 1.0), True, False)
376
+ # clean_holes = remove_small_holes(canopy_bin)
377
+ # clean_obj = remove_small_objects(clean_holes)
378
+ #
379
+ # polys = features.shapes(skimage.img_as_ubyte(clean_obj), mask=clean_obj)
380
+ # polys = [shape(poly).segmentize(FP_SEGMENTIZE_LENGTH) for poly, _ in polys]
381
+ #
382
+ # # perpendicular segments intersections with polygons
383
+ # size = corridor_raster.shape
384
+ # pts = []
385
+ # for poly in polys:
386
+ # pts.extend(list(poly.exterior.coords))
387
+ #
388
+ # index_0 = []
389
+ # index_1 = []
390
+ # for pt in pts:
391
+ # if int(pt[0]) < size[1] and int(pt[1]) < size[0]:
392
+ # index_0.append(int(pt[0]))
393
+ # index_1.append(int(pt[1]))
394
+ #
395
+ # try:
396
+ # thresholds = corridor_raster[index_1, index_0]
397
+ # except Exception as e:
398
+ # print(e)
399
+ #
400
+ # # trimmed mean of values at intersections
401
+ # threshold = stats.trim_mean(thresholds, 0.3)
402
+ #
403
+ # return threshold
404
+
405
+
406
+ def find_corridor_threshold(raster):
407
+ """
408
+ Find the optimal corridor threshold by raster histogram
409
+ Parameters
410
+ ----------
411
+ raster : corridor raster
412
+
413
+ Returns
414
+ -------
415
+ corridor_threshold : float
416
+
417
+ """
418
+ corridor_threshold = -1.0
419
+ hist, bins = np.histogram(raster.flatten(), bins=100, range=(0, 100))
420
+ CostStd = np.nanstd(raster.flatten())
421
+ half_count = np.sum(hist) / 2
422
+ sub_count = 0
423
+
424
+ for count, bin_no in zip(hist, bins):
425
+ sub_count += count
426
+ if sub_count > half_count:
427
+ break
428
+
429
+ corridor_threshold = bin_no
430
+
431
+ return corridor_threshold
432
+
433
+
434
+ def process_single_line_relative(segment):
435
+ # in_chm = rasterio.open(segment[0])
436
+
437
+ # segment[0] = in_chm
438
+ # DynCanTh = segment[1]
439
+
440
+ # Segment args from mulitprocessing:
441
+ # [clipped_chm, float(work_in_bufferR.loc[record, 'DynCanTh']), float(tree_radius),
442
+ # float(max_line_dist), float(canopy_avoidance), float(exponent), raster.res, nodata,
443
+ # line_seg.iloc[[record]], out_meta, line_id,RCut,Side,canopy_thresh_percentage,line_buffer]
444
+
445
+ # this will change segment content, and parameters will be changed
446
+ segment = dyn_canopy_cost_raster(segment)
447
+ if segment is None:
448
+ return None
449
+ # Segement after Clipped Canopy and Cost Raster
450
+ # line_df, dyn_canopy_ndarray, negative_cost_clip, out_meta, max_line_dist, nodata, line_id,Cut_Dist,line_buffer
451
+
452
+ # this function takes single line to work the line footprint
453
+ # (regardless it process the whole line or individual segment)
454
+ df = segment[0]
455
+ in_canopy_r = segment[1]
456
+ in_cost_r = segment[2]
457
+ out_meta = segment[3]
458
+
459
+ # in_transform = segment[3]
460
+ if np.isnan(in_canopy_r).all():
461
+ print("Canopy raster empty")
462
+
463
+ if np.isnan(in_cost_r).all():
464
+ print("Cost raster empty")
465
+
466
+ in_meta = segment[3]
467
+ exp_shk_cell = segment[4]
468
+ no_data = segment[5]
469
+ line_id = segment[6]
470
+ Cut_Dist = segment[7]
471
+ line_bufferR = segment[8]
472
+
473
+ shapefile_proj = df.crs
474
+ in_transform = in_meta["transform"]
475
+
476
+ FID = df["OLnSEG"] # segment line feature ID
477
+ OID = df["OLnFID"] # original line ID for segment line
478
+
479
+ segment_list = []
480
+
481
+ feat = df.geometry.iloc[0]
482
+ for coord in feat.coords:
483
+ segment_list.append(coord)
484
+
485
+ cell_size_x = in_transform[0]
486
+ cell_size_y = -in_transform[4]
487
+
488
+ # Work out the corridor from both end of the centerline
489
+ try:
490
+ # TODO: further investigate and submit issue to skimage
491
+ # There is a severe bug in skimage find_costs
492
+ # when nan is present in clip_cost_r, find_costs cause access violation
493
+ # no message/exception will be caught
494
+ # change all nan to BT_NODATA_COST for workaround
495
+ if len(in_cost_r.shape) > 2:
496
+ in_cost_r = np.squeeze(in_cost_r, axis=0)
497
+ remove_nan_from_array(in_cost_r)
498
+ in_cost_r[in_cost_r == no_data] = np.inf
499
+
500
+ # generate 1m interval points along line
501
+ distance_delta = 1
502
+ distances = np.arange(0, feat.length, distance_delta)
503
+ multipoint_along_line = [feat.interpolate(distance) for distance in distances]
504
+ multipoint_along_line.append(Point(segment_list[-1]))
505
+ # Rasterize points along line
506
+ rasterized_points_Alongln = features.rasterize(
507
+ multipoint_along_line,
508
+ out_shape=in_cost_r.shape,
509
+ transform=in_transform,
510
+ fill=0,
511
+ all_touched=True,
512
+ default_value=1,
513
+ )
514
+ points_Alongln = np.transpose(np.nonzero(rasterized_points_Alongln))
515
+
516
+ # Find minimum cost paths through an N-d costs array.
517
+ mcp_flexible1 = MCP_Flexible(in_cost_r, sampling=(cell_size_x, cell_size_y), fully_connected=True)
518
+ flex_cost_alongLn, flex_back_alongLn = mcp_flexible1.find_costs(starts=points_Alongln)
519
+
520
+ # Generate corridor
521
+ # corridor = source_cost_acc + dest_cost_acc
522
+ corridor = flex_cost_alongLn # +flex_cost_dest #cum_cost_tosource+cum_cost_todestination
523
+ corridor = np.ma.masked_invalid(corridor)
524
+
525
+ # Calculate minimum value of corridor raster
526
+ if not np.ma.min(corridor) is None:
527
+ corr_min = float(np.ma.min(corridor))
528
+ else:
529
+ corr_min = 0.5
530
+
531
+ # normalize corridor raster by deducting corr_min
532
+ corridor_norm = corridor - corr_min
533
+
534
+ # Set minimum as zero and save minimum file
535
+ # corridor_th_value = find_corridor_threshold(corridor_norm)
536
+ corridor_th_value = Cut_Dist / cell_size_x
537
+ if corridor_th_value < 0: # if no threshold found, use default value
538
+ corridor_th_value = FP_CORRIDOR_THRESHOLD / cell_size_x
539
+
540
+ # corridor_th_value = FP_CORRIDOR_THRESHOLD
541
+ corridor_thresh = np.ma.where(corridor_norm >= corridor_th_value, 1.0, 0.0)
542
+ corridor_thresh_cl = np.ma.where(corridor_norm >= (corridor_th_value + (5 / cell_size_x)), 1.0, 0.0)
543
+
544
+ # find contiguous corridor polygon for centerline
545
+ corridor_poly_gpd = find_corridor_polygon(corridor_thresh_cl, in_transform, df)
546
+
547
+ # Process: Stamp CC and Max Line Width
548
+ # Original code here
549
+ # RasterClass = SetNull(IsNull(CorridorMin),((CorridorMin) + ((Canopy_Raster) >= 1)) > 0)
550
+ temp1 = corridor_thresh + in_canopy_r
551
+ raster_class = np.ma.where(temp1 == 0, 1, 0).data
552
+
553
+ # BERA proposed Binary morphology
554
+ # RasterClass_binary=np.where(RasterClass==0,False,True)
555
+ if exp_shk_cell > 0 and cell_size_x < 1:
556
+ # Process: Expand
557
+ # FLM original Expand equivalent
558
+ cell_size = int(exp_shk_cell * 2 + 1)
559
+
560
+ expanded = ndimage.grey_dilation(raster_class, size=(cell_size, cell_size))
561
+
562
+ # BERA proposed Binary morphology Expand
563
+ # Expanded = ndimage.binary_dilation(RasterClass_binary, iterations=exp_shk_cell,border_value=1)
564
+
565
+ # Process: Shrink
566
+ # FLM original Shrink equivalent
567
+ file_shrink = ndimage.grey_erosion(expanded, size=(cell_size, cell_size))
568
+
569
+ # BERA proposed Binary morphology Shrink
570
+ # fileShrink = ndimage.binary_erosion((Expanded),iterations=Exp_Shk_cell,border_value=1)
571
+ else:
572
+ print("No Expand And Shrink cell performed.")
573
+
574
+ file_shrink = raster_class
575
+
576
+ # Process: Boundary Clean
577
+ clean_raster = ndimage.gaussian_filter(file_shrink, sigma=0, mode="nearest")
578
+
579
+ # creat mask for non-polygon area
580
+ polygon_mask = np.where(clean_raster == 1, True, False)
581
+ if clean_raster.dtype == np.int64:
582
+ clean_raster = clean_raster.astype(np.int32)
583
+
584
+ # Process: ndarray to shapely Polygon
585
+ out_polygon = features.shapes(clean_raster, mask=polygon_mask, transform=in_transform)
586
+
587
+ # create a shapely multipolygon
588
+ multi_polygon = []
589
+ for poly, value in out_polygon:
590
+ multi_polygon.append(shape(poly))
591
+ poly = MultiPolygon(multi_polygon)
592
+
593
+ # create a pandas dataframe for the FP
594
+ out_data = pd.DataFrame(
595
+ {
596
+ "OLnFID": OID,
597
+ "OLnSEG": FID,
598
+ "CorriThresh": corridor_th_value,
599
+ "geometry": poly,
600
+ }
601
+ )
602
+ out_gdata = GeoDataFrame(out_data, geometry="geometry", crs=shapefile_proj)
603
+
604
+ return out_gdata, corridor_poly_gpd
605
+
606
+ except Exception as e:
607
+ print("Exception: {}".format(e))
608
+
609
+
610
+ def multiprocessing_footprint_relative(line_args, processes):
611
+ try:
612
+ total_steps = len(line_args)
613
+
614
+ feats = []
615
+ # chunksize = math.ceil(total_steps / processes)
616
+ with Pool(processes=processes) as pool:
617
+ step = 0
618
+ # execute tasks in order, process results out of order
619
+ for result in pool.imap_unordered(process_single_line_relative, line_args):
620
+ if BT_DEBUGGING:
621
+ print("Got result: {}".format(result), flush=True)
622
+ if result != None:
623
+ feats.append(result)
624
+ step += 1
625
+ print(
626
+ ' "PROGRESS_LABEL Dynamic Segment Line Footprint {} of {}" '.format(step, total_steps),
627
+ flush=True,
628
+ )
629
+ print(" %{} ".format(step / total_steps * 100), flush=True)
630
+ return feats
631
+ except OperationCancelledException:
632
+ print("Operation cancelled")
633
+ return None
634
+
635
+
636
+ def main_line_footprint_relative(
637
+ callback,
638
+ in_line,
639
+ in_chm,
640
+ max_ln_width,
641
+ exp_shk_cell,
642
+ out_footprint,
643
+ out_centerline,
644
+ tree_radius,
645
+ max_line_dist,
646
+ canopy_avoidance,
647
+ exponent,
648
+ full_step,
649
+ canopy_thresh_percentage,
650
+ processes,
651
+ verbose,
652
+ ):
653
+ # use_corridor_th_col = True
654
+ line_seg = GeoDataFrame.from_file(in_line)
655
+
656
+ # If Dynamic canopy threshold column not found, create one
657
+ if "DynCanTh" not in line_seg.columns.array:
658
+ print("Please create field {} first".format("DynCanTh"))
659
+ exit()
660
+ if not float(canopy_thresh_percentage):
661
+ canopy_thresh_percentage = 50
662
+ else:
663
+ canopy_thresh_percentage = float(canopy_thresh_percentage)
664
+
665
+ if float(canopy_avoidance) <= 0.0:
666
+ canopy_avoidance = 0.0
667
+ if float(exponent) <= 0.0:
668
+ exponent = 1.0
669
+ # If OLnFID column is not found, column will be created
670
+ if "OLnFID" not in line_seg.columns.array:
671
+ print("Created {} column in input line data.".format("OLnFID"))
672
+ line_seg["OLnFID"] = line_seg.index
673
+
674
+ if "OLnSEG" not in line_seg.columns.array:
675
+ line_seg["OLnSEG"] = 0
676
+
677
+ print("%{}".format(10))
678
+
679
+ # check coordinate systems between line and raster features
680
+ with rasterio.open(in_chm) as raster:
681
+ line_args = []
682
+
683
+ if compare_crs(vector_crs(in_line), raster_crs(in_chm)):
684
+ proc_segments = False
685
+ if proc_segments:
686
+ print("Splitting lines into segments...")
687
+ line_seg_split = split_into_segments(line_seg)
688
+ print("Splitting lines into segments...Done")
689
+ else:
690
+ if full_step:
691
+ print("Tool runs on input lines......")
692
+ line_seg_split = line_seg
693
+ else:
694
+ print("Tool runs on input segment lines......")
695
+ line_seg_split = split_into_equal_Nth_segments(line_seg, 250)
696
+
697
+ print("%{}".format(20))
698
+
699
+ work_in_bufferL1 = GeoDataFrame.copy(line_seg_split)
700
+ work_in_bufferL2 = GeoDataFrame.copy(line_seg_split)
701
+ work_in_bufferR1 = GeoDataFrame.copy(line_seg_split)
702
+ work_in_bufferR2 = GeoDataFrame.copy(line_seg_split)
703
+ work_in_bufferC = GeoDataFrame.copy(line_seg_split)
704
+ work_in_bufferL1["geometry"] = buffer(
705
+ work_in_bufferL1["geometry"],
706
+ distance=float(max_ln_width) + 1,
707
+ cap_style=3,
708
+ single_sided=True,
709
+ )
710
+
711
+ work_in_bufferL2["geometry"] = buffer(
712
+ work_in_bufferL2["geometry"],
713
+ distance=-1,
714
+ cap_style=3,
715
+ single_sided=True,
716
+ )
717
+
718
+ work_in_bufferL = GeoDataFrame(pd.concat([work_in_bufferL1, work_in_bufferL2]))
719
+ work_in_bufferL = work_in_bufferL.dissolve(by=["OLnFID", "OLnSEG"], as_index=False)
720
+
721
+ work_in_bufferR1["geometry"] = buffer(
722
+ work_in_bufferR1["geometry"],
723
+ distance=-float(max_ln_width) - 1,
724
+ cap_style=3,
725
+ single_sided=True,
726
+ )
727
+ work_in_bufferR2["geometry"] = buffer(
728
+ work_in_bufferR2["geometry"], distance=1, cap_style=3, single_sided=True
729
+ )
730
+
731
+ work_in_bufferR = GeoDataFrame(pd.concat([work_in_bufferR1, work_in_bufferR2]))
732
+ work_in_bufferR = work_in_bufferR.dissolve(by=["OLnFID", "OLnSEG"], as_index=False)
733
+
734
+ work_in_bufferC["geometry"] = buffer(
735
+ work_in_bufferC["geometry"],
736
+ distance=float(max_ln_width),
737
+ cap_style=3,
738
+ single_sided=False,
739
+ )
740
+ print("Prepare arguments for Dynamic FP ...")
741
+ # line_argsL, line_argsR,line_argsC= generate_line_args(line_seg_split, work_in_bufferL,work_in_bufferC,
742
+ # raster, tree_radius, max_line_dist,
743
+ # canopy_avoidance, exponent, work_in_bufferR,
744
+ # canopy_thresh_percentage)
745
+
746
+ line_argsL, line_argsR, line_argsC = generate_line_args_DFP_NoClip(
747
+ line_seg_split,
748
+ work_in_bufferL,
749
+ work_in_bufferC,
750
+ raster,
751
+ in_chm,
752
+ tree_radius,
753
+ max_line_dist,
754
+ canopy_avoidance,
755
+ exponent,
756
+ work_in_bufferR,
757
+ canopy_thresh_percentage,
758
+ )
759
+
760
+ else:
761
+ print("Line and canopy raster spatial references are not same, please check.")
762
+ exit()
763
+ # pass center lines for footprint
764
+ print("Generating Dynamic footprint ...")
765
+
766
+ feat_listL = []
767
+ feat_listR = []
768
+ feat_listC = []
769
+ poly_listL = []
770
+ poly_listR = []
771
+ footprint_listL = []
772
+ footprint_listR = []
773
+ footprint_listC = []
774
+ # PARALLEL_MODE = ParallelMode.SEQUENTIAL
775
+ if PARALLEL_MODE == ParallelMode.MULTIPROCESSING:
776
+ # feat_listC = multiprocessing_footprint_relative(line_argsC, processes)
777
+ feat_listL = multiprocessing_footprint_relative(line_argsL, processes)
778
+ # feat_listL = execute_multiprocessing(process_single_line_relative,'Footprint',line_argsL, processes)
779
+ feat_listR = multiprocessing_footprint_relative(line_argsR, processes)
780
+ # feat_listR = execute_multiprocessing(process_single_line_relative, 'Footprint', line_argsR, processes)
781
+
782
+ elif PARALLEL_MODE == ParallelMode.SEQUENTIAL:
783
+ step = 1
784
+ total_steps = len(line_argsL)
785
+ print("There are {} result to process.".format(total_steps))
786
+ for row in line_argsL:
787
+ feat_listL.append(process_single_line_relative(row))
788
+ print("Footprint (left side) for line {} is done".format(step))
789
+ print(
790
+ ' "PROGRESS_LABEL Dynamic Line Footprint {} of {}" '.format(step, total_steps),
791
+ flush=True,
792
+ )
793
+ print(" %{} ".format((step / total_steps) * 100))
794
+ step += 1
795
+ step = 1
796
+ total_steps = len(line_argsR)
797
+ for row in line_argsR:
798
+ feat_listR.append(process_single_line_relative(row))
799
+ print("Footprint for (right side) line {} is done".format(step))
800
+ print(
801
+ ' "PROGRESS_LABEL Dynamic Line Footprint {} of {}" '.format(step, total_steps),
802
+ flush=True,
803
+ )
804
+ print(" %{} ".format((step / total_steps) * 100))
805
+ step += 1
806
+
807
+ print("%{}".format(80))
808
+ print("Task done.")
809
+
810
+ for feat in feat_listL:
811
+ if feat:
812
+ footprint_listL.append(feat[0])
813
+ poly_listL.append(feat[1])
814
+
815
+ for feat in feat_listR:
816
+ if feat:
817
+ footprint_listR.append(feat[0])
818
+ poly_listR.append(feat[1])
819
+
820
+ print("Writing shapefile ...")
821
+ resultsL = GeoDataFrame(pd.concat(footprint_listL))
822
+ resultsL["geometry"] = resultsL["geometry"].buffer(0.005)
823
+ resultsR = GeoDataFrame(pd.concat(footprint_listR))
824
+ resultsR["geometry"] = resultsR["geometry"].buffer(0.005)
825
+ resultsL = resultsL.sort_values(by=["OLnFID", "OLnSEG"])
826
+ resultsR = resultsR.sort_values(by=["OLnFID", "OLnSEG"])
827
+ resultsL = resultsL.reset_index(drop=True)
828
+ resultsR = resultsR.reset_index(drop=True)
829
+ #
830
+
831
+ resultsAll = GeoDataFrame(pd.concat([resultsL, resultsR]))
832
+ dissolved_results = resultsAll.dissolve(by="OLnFID", as_index=False)
833
+ dissolved_results["geometry"] = dissolved_results["geometry"].buffer(-0.005)
834
+ print("Saving output ...")
835
+ dissolved_results.to_file(out_footprint)
836
+ print("Footprint file saved")
837
+
838
+ # dissolved polygon group by column 'OLnFID'
839
+ print("Generating centerlines from corridor polygons ...")
840
+ resultsCL = GeoDataFrame(pd.concat(poly_listL))
841
+ resultsCL["geometry"] = resultsCL["geometry"].buffer(0.005)
842
+ resultsCR = GeoDataFrame(pd.concat(poly_listR))
843
+ resultsCR["geometry"] = resultsCR["geometry"].buffer(0.005)
844
+
845
+ resultsCLR = GeoDataFrame(pd.concat([resultsCL, resultsCR]))
846
+ resultsCLR = resultsCLR.dissolve(by="OLnFID", as_index=False)
847
+ resultsCLR = resultsCLR.sort_values(by=["OLnFID", "OLnSEG"])
848
+ resultsCLR = resultsCLR.reset_index(drop=True)
849
+ resultsCLR["geometry"] = resultsCLR["geometry"].buffer(-0.005)
850
+
851
+ # out_centerline=False
852
+ # save lines to file
853
+ if out_centerline:
854
+ poly_centerline_gpd = find_centerlines(resultsCLR, line_seg, processes)
855
+ poly_gpd = poly_centerline_gpd.copy()
856
+ centerline_gpd = poly_centerline_gpd.copy()
857
+
858
+ centerline_gpd = centerline_gpd.set_geometry("centerline")
859
+ centerline_gpd = centerline_gpd.drop(columns=["geometry"])
860
+ centerline_gpd.crs = poly_centerline_gpd.crs
861
+ centerline_gpd.to_file(out_centerline)
862
+ print("Centerline file saved")
863
+
864
+ # save polygons
865
+ path = Path(out_centerline)
866
+ path = path.with_stem(path.stem + "_poly")
867
+ poly_gpd = poly_gpd.drop(columns=["centerline"])
868
+ poly_gpd.to_file(path)
869
+
870
+ print("%{}".format(100))
871
+
872
+
873
+ if __name__ == "__main__":
874
+ start_time = time.time()
875
+ print("Dynamic Footprint processing started")
876
+ print("Current time: {}".format(time.strftime("%d %b %Y %H:%M:%S", time.localtime())))
877
+
878
+ in_args, in_verbose = check_arguments()
879
+ main_line_footprint_relative(print, **in_args.input, processes=int(in_args.processes), verbose=in_verbose)
880
+
881
+ print("%{}".format(100))
882
+ print("Dynamic Footprint processing finished")
883
+ print("Current time: {}".format(time.strftime("%d %b %Y %H:%M:%S", time.localtime())))
884
+ print("Total processing time (seconds): {}".format(round(time.time() - start_time, 3)))