BERATools 0.2.3__py3-none-any.whl → 0.2.4__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 (78) hide show
  1. beratools/__init__.py +8 -3
  2. beratools/core/{algo_footprint_rel.py → algo_canopy_footprint_exp.py} +176 -139
  3. beratools/core/algo_centerline.py +61 -77
  4. beratools/core/algo_common.py +48 -57
  5. beratools/core/algo_cost.py +18 -25
  6. beratools/core/algo_dijkstra.py +37 -45
  7. beratools/core/algo_line_grouping.py +100 -100
  8. beratools/core/algo_merge_lines.py +40 -8
  9. beratools/core/algo_split_with_lines.py +289 -304
  10. beratools/core/algo_vertex_optimization.py +25 -46
  11. beratools/core/canopy_threshold_relative.py +755 -0
  12. beratools/core/constants.py +8 -9
  13. beratools/{tools → core}/line_footprint_functions.py +411 -258
  14. beratools/core/logger.py +18 -2
  15. beratools/core/tool_base.py +17 -75
  16. beratools/gui/assets/BERALogo.ico +0 -0
  17. beratools/gui/assets/BERA_Splash.gif +0 -0
  18. beratools/gui/assets/BERA_WizardImage.png +0 -0
  19. beratools/gui/assets/beratools.json +475 -2171
  20. beratools/gui/bt_data.py +585 -234
  21. beratools/gui/bt_gui_main.py +129 -91
  22. beratools/gui/main.py +4 -7
  23. beratools/gui/tool_widgets.py +530 -354
  24. beratools/tools/__init__.py +0 -7
  25. beratools/tools/{line_footprint_absolute.py → canopy_footprint_absolute.py} +81 -56
  26. beratools/tools/canopy_footprint_exp.py +113 -0
  27. beratools/tools/centerline.py +30 -37
  28. beratools/tools/check_seed_line.py +127 -0
  29. beratools/tools/common.py +65 -586
  30. beratools/tools/{line_footprint_fixed.py → ground_footprint.py} +140 -117
  31. beratools/tools/line_footprint_relative.py +64 -35
  32. beratools/tools/tool_template.py +48 -40
  33. beratools/tools/vertex_optimization.py +20 -34
  34. beratools/utility/env_checks.py +53 -0
  35. beratools/utility/spatial_common.py +210 -0
  36. beratools/utility/tool_args.py +138 -0
  37. beratools-0.2.4.dist-info/METADATA +134 -0
  38. beratools-0.2.4.dist-info/RECORD +50 -0
  39. {beratools-0.2.3.dist-info → beratools-0.2.4.dist-info}/WHEEL +1 -1
  40. beratools-0.2.4.dist-info/entry_points.txt +3 -0
  41. beratools-0.2.4.dist-info/licenses/LICENSE +674 -0
  42. beratools/core/algo_tiler.py +0 -428
  43. beratools/gui/__init__.py +0 -11
  44. beratools/gui/batch_processing_dlg.py +0 -513
  45. beratools/gui/map_window.py +0 -162
  46. beratools/tools/Beratools_r_script.r +0 -1120
  47. beratools/tools/Ht_metrics.py +0 -116
  48. beratools/tools/batch_processing.py +0 -136
  49. beratools/tools/canopy_threshold_relative.py +0 -672
  50. beratools/tools/canopycostraster.py +0 -222
  51. beratools/tools/fl_regen_csf.py +0 -428
  52. beratools/tools/forest_line_attributes.py +0 -408
  53. beratools/tools/line_grouping.py +0 -45
  54. beratools/tools/ln_relative_metrics.py +0 -615
  55. beratools/tools/r_cal_lpi_elai.r +0 -25
  56. beratools/tools/r_generate_pd_focalraster.r +0 -101
  57. beratools/tools/r_interface.py +0 -80
  58. beratools/tools/r_point_density.r +0 -9
  59. beratools/tools/rpy_chm2trees.py +0 -86
  60. beratools/tools/rpy_dsm_chm_by.py +0 -81
  61. beratools/tools/rpy_dtm_by.py +0 -63
  62. beratools/tools/rpy_find_cellsize.py +0 -43
  63. beratools/tools/rpy_gnd_csf.py +0 -74
  64. beratools/tools/rpy_hummock_hollow.py +0 -85
  65. beratools/tools/rpy_hummock_hollow_raster.py +0 -71
  66. beratools/tools/rpy_las_info.py +0 -51
  67. beratools/tools/rpy_laz2las.py +0 -40
  68. beratools/tools/rpy_lpi_elai_lascat.py +0 -466
  69. beratools/tools/rpy_normalized_lidar_by.py +0 -56
  70. beratools/tools/rpy_percent_above_dbh.py +0 -80
  71. beratools/tools/rpy_points2trees.py +0 -88
  72. beratools/tools/rpy_vegcoverage.py +0 -94
  73. beratools/tools/tiler.py +0 -48
  74. beratools/tools/zonal_threshold.py +0 -144
  75. beratools-0.2.3.dist-info/METADATA +0 -108
  76. beratools-0.2.3.dist-info/RECORD +0 -74
  77. beratools-0.2.3.dist-info/entry_points.txt +0 -2
  78. beratools-0.2.3.dist-info/licenses/LICENSE +0 -22
beratools/__init__.py CHANGED
@@ -1,3 +1,8 @@
1
- __author__ = """AppliedGRG"""
2
- __email__ = 'appliedgrg@gmail.com'
3
- __version__ = '0.2.3'
1
+ __author__ = """AppliedGRG"""
2
+ __email__ = 'appliedgrg@gmail.com'
3
+ __version__ = '0.1.0'
4
+ __license__ = "GPL-3.0-or-later"
5
+ __copyright__ = "Copyright (c) AppliedGRG"
6
+ __status__ = "Pre-Alpha"
7
+ __url__ = "https://github.com/appliedgrg/beratools"
8
+ __all__ = ["tools", "gui"]
@@ -10,12 +10,12 @@ Description:
10
10
  This script is part of the BERA Tools.
11
11
  Webpage: https://github.com/appliedgrg/beratools
12
12
 
13
- The purpose of this script is to provide main interface for canopy footprint tool.
14
- The tool is used to generate the footprint of a line based on relative threshold.
13
+ The purpose of this script is to provide main interface for experimental canopy footprint tool.
14
+ The tool is used to generate the canopy footprint of a line based on relative threshold.
15
15
  """
16
+
16
17
  import math
17
- import time
18
- from enum import StrEnum
18
+ from enum import Enum
19
19
 
20
20
  import geopandas as gpd
21
21
  import numpy as np
@@ -30,47 +30,79 @@ import beratools.core.algo_common as algo_common
30
30
  import beratools.core.algo_cost as algo_cost
31
31
  import beratools.core.constants as bt_const
32
32
  import beratools.core.tool_base as bt_base
33
- import beratools.tools.common as bt_common
33
+ import beratools.utility.spatial_common as sp_common
34
34
 
35
35
 
36
- class Side(StrEnum):
36
+ class Side(Enum):
37
37
  """Constants for left and right side."""
38
38
 
39
39
  left = "left"
40
40
  right = "right"
41
41
 
42
+
42
43
  class FootprintCanopy:
43
44
  """Relative canopy footprint class."""
44
45
 
45
- def __init__(self, in_geom, in_chm, in_layer=None):
46
- data = gpd.read_file(in_geom, layer=in_layer)
46
+ def __init__(self, in_geom, in_chm):
47
+ in_file, in_layer = sp_common.decode_file_layer(in_geom)
48
+ data = gpd.read_file(in_file, layer=in_layer)
47
49
  self.lines = []
48
50
 
49
51
  for idx in data.index:
50
52
  line = LineInfo(data.iloc[[idx]], in_chm)
51
53
  self.lines.append(line)
52
54
 
53
- def compute(self, processes, parallel_mode=bt_const.ParallelMode.MULTIPROCESSING):
55
+ def compute(self, processes):
54
56
  result = bt_base.execute_multiprocessing(
55
57
  algo_common.process_single_item,
56
58
  self.lines,
57
59
  "Canopy Footprint",
58
60
  processes,
59
- 1,
60
- parallel_mode,
61
61
  )
62
62
 
63
- fp = [item.footprint for item in result]
64
- self.footprints = pd.concat(fp)
63
+ fp = []
64
+ percentile = []
65
+ try:
66
+ for item in result:
67
+ if item.footprint is not None:
68
+ fp.append(item.footprint)
69
+ else:
70
+ print("Footprint is None for one of the lines.")
71
+ continue # Skip failed line
72
+
73
+ if fp:
74
+ self.footprints = pd.concat(fp)
75
+ else:
76
+ print("No valid footprints to save.")
77
+ self.footprints = None
78
+
79
+ for item in result:
80
+ if item.lines_percentile is not None:
81
+ percentile.append(item.lines_percentile)
82
+ else:
83
+ print("lines_percentile is None for one of the lines.")
84
+ continue # Skip failed line
65
85
 
66
- percentile = [item.line for item in result]
67
- self.lines_percentile = pd.concat(percentile)
86
+ if percentile:
87
+ self.lines_percentile = pd.concat(percentile)
88
+ else:
89
+ print("No valid lines_percentile to save.")
90
+ self.lines_percentile = None
91
+ except Exception as e:
92
+ print(f"Error during processing: {e}")
68
93
 
69
94
  def save_footprint(self, out_footprint, layer=None):
70
- self.footprints.to_file(out_footprint, layer=layer)
95
+ if self.footprints is not None and isinstance(self.footprints, gpd.GeoDataFrame):
96
+ self.footprints.to_file(out_footprint, layer=layer)
97
+ else:
98
+ print("No footprints to save (None or not a GeoDataFrame).")
71
99
 
72
100
  def save_line_percentile(self, out_percentile):
73
- self.lines_percentile.to_file(out_percentile)
101
+ if self.lines_percentile is not None and isinstance(self.lines_percentile, gpd.GeoDataFrame):
102
+ self.lines_percentile.to_file(out_percentile)
103
+ else:
104
+ print("No lines_percentile to save (None or not a GeoDataFrame).")
105
+
74
106
 
75
107
  class BufferRing:
76
108
  """Buffer ring class."""
@@ -81,28 +113,27 @@ class BufferRing:
81
113
  self.percentile = 0.5
82
114
  self.Dyn_Canopy_Threshold = 0.05
83
115
 
116
+
84
117
  class LineInfo:
85
118
  """Class to store line information."""
86
119
 
87
- def __init__(self,
88
- line_gdf, in_chm,
89
- max_ln_width=32,
90
- tree_radius=1.5,
91
- max_line_dist=1.5,
92
- canopy_avoidance=0.0,
93
- exponent=1.0,
94
- canopy_thresh_percentage=50):
120
+ def __init__(
121
+ self,
122
+ line_gdf,
123
+ in_chm,
124
+ max_ln_width=32,
125
+ tree_radius=1.5,
126
+ max_line_dist=1.5,
127
+ canopy_avoidance=0.0,
128
+ exponent=1.0,
129
+ canopy_thresh_percentage=50,
130
+ ):
95
131
  self.line = line_gdf
96
132
  self.in_chm = in_chm
97
- self.line_simp = self.line.geometry.simplify(
98
- tolerance=0.5, preserve_topology=True
99
- )
133
+ self.line_simp = self.line.geometry.simplify(tolerance=0.5, preserve_topology=True)
100
134
 
101
135
  self.canopy_percentile = 50
102
136
  self.DynCanTh = np.nan
103
- # chk_df_multipart
104
- # if proc_segments:
105
- # line_seg = split_into_segments(line_seg)
106
137
 
107
138
  self.buffer_rings = []
108
139
 
@@ -127,16 +158,38 @@ class LineInfo:
127
158
  self.buffer_right = None
128
159
  self.footprint = None
129
160
 
161
+ self.lines_percentile = None
162
+
130
163
  def compute(self):
131
164
  self.prepare_ring_buffer()
132
165
 
133
166
  ring_list = []
134
167
  for item in self.buffer_rings:
135
168
  ring = self.cal_percentileRing(item)
136
- ring_list.append(ring)
169
+ if ring is not None:
170
+ ring_list.append(ring)
171
+ else:
172
+ # print("Skipping invalid ring.")
173
+ # TODO: handle None rings appropriately
174
+ pass
137
175
 
138
176
  self.buffer_rings = ring_list
139
177
 
178
+ # Aggregate percentiles and geometries for lines_percentile
179
+ percentile_records = []
180
+ for ring in self.buffer_rings:
181
+ if ring is not None and hasattr(ring, "geometry") and hasattr(ring, "percentile"):
182
+ percentile_records.append(
183
+ {"geometry": ring.geometry, "percentile": ring.percentile, "side": ring.side.value}
184
+ )
185
+ if percentile_records:
186
+ self.lines_percentile = gpd.GeoDataFrame(percentile_records)
187
+ self.lines_percentile.set_geometry("geometry", inplace=True)
188
+ if self.line.crs:
189
+ self.lines_percentile = self.lines_percentile.set_crs(self.line.crs, allow_override=True)
190
+ else:
191
+ self.lines_percentile = None
192
+
140
193
  self.rate_of_change(self.get_percentile_array(Side.left), Side.left)
141
194
  self.rate_of_change(self.get_percentile_array(Side.right), Side.right)
142
195
 
@@ -152,14 +205,35 @@ class LineInfo:
152
205
 
153
206
  fp_left = self.process_single_footprint(Side.left)
154
207
  fp_right = self.process_single_footprint(Side.right)
155
- fp_left.geometry = fp_left.buffer(0.005)
156
- fp_right.geometry = fp_right.buffer(0.005)
157
- self.footprint = pd.concat([fp_left, fp_right])
158
- self.footprint = self.footprint.dissolve()
159
- self.footprint.geometry = self.footprint.buffer(-0.005)
160
-
161
- # transfer group value to footprint
162
- # TODO: lines and footprint can match by index
208
+
209
+ # Check if footprints are valid
210
+ if fp_left is None or fp_right is None:
211
+ print("One or both footprints are None in LineInfo.")
212
+ self.footprint = None
213
+ return
214
+
215
+ try:
216
+ # Buffer cleanup for validity
217
+ fp_left.geometry = fp_left.geometry.buffer(0)
218
+ fp_right.geometry = fp_right.geometry.buffer(0)
219
+
220
+ fp_combined = pd.concat([fp_left, fp_right])
221
+
222
+ if fp_combined.empty or not isinstance(fp_combined, gpd.GeoDataFrame):
223
+ print("Combined footprint is invalid or empty.")
224
+ self.footprint = None
225
+ return
226
+
227
+ fp_combined = fp_combined.dissolve()
228
+ fp_combined.geometry = fp_combined.geometry.buffer(-0.005)
229
+
230
+ self.footprint = fp_combined
231
+ except Exception as e:
232
+ print(f"Error combining footprints: {e}")
233
+ self.footprint = None
234
+ return
235
+
236
+ # Transfer group value to footprint if present
163
237
  if bt_const.BT_GROUP in self.line.columns:
164
238
  self.footprint[bt_const.BT_GROUP] = self.line[bt_const.BT_GROUP].iloc[0]
165
239
 
@@ -183,27 +257,25 @@ class LineInfo:
183
257
  print("Empty buffer ring")
184
258
 
185
259
  def cal_percentileRing(self, ring):
260
+ line_buffer = None
186
261
  try:
187
262
  line_buffer = ring.geometry
188
263
  if line_buffer.is_empty or shapely.is_missing(line_buffer):
189
264
  return None
190
265
  if line_buffer.has_z:
191
- line_buffer = sh_ops.transform(
192
- lambda x, y, z=None: (x, y), line_buffer
193
- )
266
+ line_buffer = sh_ops.transform(lambda x, y, z=None: (x, y), line_buffer)
194
267
 
195
268
  except Exception as e:
196
269
  print(f"cal_percentileRing: {e}")
270
+ return None
197
271
 
198
272
  # TODO: temporary workaround for exception causing not percentile defined
199
273
  try:
200
- clipped_raster, _ = bt_common.clip_raster(self.in_chm, line_buffer, 0)
274
+ clipped_raster, _ = sp_common.clip_raster(self.in_chm, line_buffer, 0)
201
275
  clipped_raster = np.squeeze(clipped_raster, axis=0)
202
276
 
203
277
  # mask all -9999 (nodata) value cells
204
- masked_raster = np.ma.masked_where(
205
- clipped_raster == bt_const.BT_NODATA, clipped_raster
206
- )
278
+ masked_raster = np.ma.masked_where(clipped_raster == bt_const.BT_NODATA, clipped_raster)
207
279
  filled_raster = np.ma.filled(masked_raster, np.nan)
208
280
 
209
281
  # Calculate the percentile
@@ -288,7 +360,7 @@ class LineInfo:
288
360
  except IndexError:
289
361
  pass
290
362
 
291
- # if still no result found, lower to 10% (1.1),
363
+ # if still no result found, lower to 10% (1.1),
292
364
  # if no result found then default is used
293
365
  if not found:
294
366
  if 0.5 >= median_percentile:
@@ -318,31 +390,25 @@ class LineInfo:
318
390
  """
319
391
  Buffers an input DataFrames geometry nring (number of rings) times.
320
392
 
321
- Compute with a distance between rings of ringdist and returns
393
+ Compute with a distance between rings of ringdist and returns
322
394
  a list of non overlapping buffers
323
395
  """
324
396
  rings = [] # A list to hold the individual buffers
325
397
  line = df.geometry.iloc[0]
326
398
  # For each ring (1, 2, 3, ..., nrings)
327
- for ring in np.arange(0, ringdist, nrings):
399
+ for ring in np.arange(0, ringdist, nrings):
328
400
  big_ring = line.buffer(
329
401
  nrings + ring, single_sided=True, cap_style="flat"
330
402
  ) # Create one big buffer
331
- small_ring = line.buffer(
332
- ring, single_sided=True, cap_style="flat"
333
- ) # Create one smaller one
334
- the_ring = big_ring.difference(
335
- small_ring
336
- ) # Difference the big with the small to create a ring
403
+ small_ring = line.buffer(ring, single_sided=True, cap_style="flat") # Create one smaller one
404
+ the_ring = big_ring.difference(small_ring) # Difference the big with the small to create a ring
337
405
  if (
338
406
  ~shapely.is_empty(the_ring)
339
407
  or ~shapely.is_missing(the_ring)
340
408
  or not None
341
409
  or ~the_ring.area == 0
342
410
  ):
343
- if isinstance(the_ring, sh_geom.MultiPolygon) or isinstance(
344
- the_ring, shapely.Polygon
345
- ):
411
+ if isinstance(the_ring, sh_geom.MultiPolygon) or isinstance(the_ring, shapely.Polygon):
346
412
  rings.append(the_ring) # Append the ring to the rings list
347
413
  else:
348
414
  if isinstance(the_ring, shapely.GeometryCollection):
@@ -379,40 +445,32 @@ class LineInfo:
379
445
 
380
446
  def dyn_canopy_cost_raster(self, side):
381
447
  in_chm_raster = self.in_chm
382
- # tree_radius = self.tree_radius
383
- # max_line_dist = self.max_line_dist
384
- # canopy_avoid = self.canopy_avoidance
385
- # exponent = self.exponent
386
448
  line_df = self.line
387
449
  out_meta = self.out_meta
388
450
 
389
451
  canopy_thresh_percentage = self.canopy_thresh_percentage / 100
390
452
 
453
+ Cut_Dist = None
454
+ line_buffer = None
391
455
  if side == Side.left:
392
- canopy_ht_threshold = line_df.CL_CutHt * canopy_thresh_percentage
456
+ canopy_ht_threshold = line_df.CL_CutHt.iloc[0] * canopy_thresh_percentage
393
457
  Cut_Dist = self.LDist_Cut
394
458
  line_buffer = self.buffer_left
395
459
  elif side == Side.right:
396
- canopy_ht_threshold = line_df.CR_CutHt * canopy_thresh_percentage
460
+ canopy_ht_threshold = line_df.CR_CutHt.iloc[0] * canopy_thresh_percentage
397
461
  Cut_Dist = self.RDist_Cut
398
462
  line_buffer = self.buffer_right
399
463
  else:
400
464
  canopy_ht_threshold = 0.5
465
+ Cut_Dist = 1.0
466
+ line_buffer = None
401
467
 
402
468
  canopy_ht_threshold = float(canopy_ht_threshold)
403
469
  if canopy_ht_threshold <= 0:
404
470
  canopy_ht_threshold = 0.5
405
471
 
406
- # get the round up integer number for tree search radius
407
- # tree_radius = float(tree_radius)
408
- # max_line_dist = float(max_line_dist)
409
- # canopy_avoid = float(canopy_avoid)
410
- # cost_raster_exponent = float(exponent)
411
-
412
472
  try:
413
- clipped_rasterC, out_meta = bt_common.clip_raster(
414
- in_chm_raster, line_buffer, 0
415
- )
473
+ clipped_rasterC, out_meta = sp_common.clip_raster(in_chm_raster, line_buffer, 0)
416
474
  negative_cost_clip, dyn_canopy_ndarray = algo_cost.cost_raster(
417
475
  clipped_rasterC,
418
476
  out_meta,
@@ -431,13 +489,21 @@ class LineInfo:
431
489
 
432
490
  def process_single_footprint(self, side):
433
491
  # this will change segment content, and parameters will be changed
434
- in_canopy_r, in_cost_r, in_meta, Cut_Dist = self.dyn_canopy_cost_raster(side)
492
+ result = self.dyn_canopy_cost_raster(side)
493
+ if result is None:
494
+ return None
495
+ in_canopy_r, in_cost_r, in_meta, Cut_Dist = result
496
+
497
+ if in_canopy_r is None or in_cost_r is None or in_meta is None or Cut_Dist is None:
498
+ return None
435
499
 
436
500
  if np.isnan(in_canopy_r).all():
437
501
  print("Canopy raster empty")
502
+ return None
438
503
 
439
504
  if np.isnan(in_cost_r).all():
440
505
  print("Cost raster empty")
506
+ return None
441
507
 
442
508
  exp_shk_cell = self.exponent # TODO: duplicate vars
443
509
  no_data = self.nodata
@@ -448,8 +514,13 @@ class LineInfo:
448
514
  segment_list = []
449
515
 
450
516
  feat = self.line.geometry.iloc[0]
451
- for coord in feat.coords:
452
- segment_list.append(coord)
517
+ if hasattr(feat, "geoms"):
518
+ for geom in feat.geoms:
519
+ for coord in geom.coords:
520
+ segment_list.append(coord)
521
+ else:
522
+ for coord in feat.coords:
523
+ segment_list.append(coord)
453
524
 
454
525
  cell_size_x = in_transform[0]
455
526
  cell_size_y = -in_transform[4]
@@ -465,9 +536,7 @@ class LineInfo:
465
536
  # generate 1m interval points along line
466
537
  distance_delta = 1
467
538
  distances = np.arange(0, feat.length, distance_delta)
468
- multipoint_along_line = [
469
- feat.interpolate(distance) for distance in distances
470
- ]
539
+ multipoint_along_line = [feat.interpolate(distance) for distance in distances]
471
540
  multipoint_along_line.append(sh_geom.Point(segment_list[-1]))
472
541
  # Rasterize points along line
473
542
  rasterized_points_Alongln = ras_feat.rasterize(
@@ -481,12 +550,8 @@ class LineInfo:
481
550
  points_Alongln = np.transpose(np.nonzero(rasterized_points_Alongln))
482
551
 
483
552
  # Find minimum cost paths through an N-d costs array.
484
- mcp_flexible1 = MCP_Flexible(
485
- in_cost_r, sampling=(cell_size_x, cell_size_y), fully_connected=True
486
- )
487
- flex_cost_alongLn, flex_back_alongLn = mcp_flexible1.find_costs(
488
- starts=points_Alongln
489
- )
553
+ mcp_flexible1 = MCP_Flexible(in_cost_r, sampling=(cell_size_x, cell_size_y), fully_connected=True)
554
+ flex_cost_alongLn, flex_back_alongLn = mcp_flexible1.find_costs(starts=points_Alongln)
490
555
 
491
556
  # Generate corridor
492
557
  corridor = flex_cost_alongLn
@@ -507,9 +572,7 @@ class LineInfo:
507
572
  corridor_th_value = bt_const.FP_CORRIDOR_THRESHOLD / cell_size_x
508
573
 
509
574
  corridor_thresh = np.ma.where(corridor_norm >= corridor_th_value, 1.0, 0.0)
510
- clean_raster = algo_common.morph_raster(
511
- corridor_thresh, in_canopy_r, exp_shk_cell, cell_size_x
512
- )
575
+ clean_raster = algo_common.morph_raster(corridor_thresh, in_canopy_r, exp_shk_cell, cell_size_x)
513
576
 
514
577
  # create mask for non-polygon area
515
578
  mask = np.where(clean_raster == 1, True, False)
@@ -517,61 +580,35 @@ class LineInfo:
517
580
  clean_raster = clean_raster.astype(np.int32)
518
581
 
519
582
  # Process: ndarray to shapely Polygon
520
- out_polygon = ras_feat.shapes(
521
- clean_raster, mask=mask, transform=in_transform
522
- )
583
+ out_polygon = ras_feat.shapes(clean_raster, mask=mask, transform=in_transform)
523
584
 
524
585
  # create a shapely MultiPolygon
525
586
  multi_polygon = []
526
- for poly, value in out_polygon:
527
- multi_polygon.append(sh_geom.shape(poly))
528
- poly = sh_geom.MultiPolygon(multi_polygon)
529
-
530
- # create a pandas DataFrame for the FP
531
- out_data = pd.DataFrame(
532
- {
533
- "CorriThresh": [corridor_th_value],
534
- "geometry": [poly]
535
- }
536
- )
537
- out_gdata = gpd.GeoDataFrame(
538
- out_data, geometry="geometry", crs=shapefile_proj
539
- )
587
+ if out_polygon is not None:
588
+ try:
589
+ for poly, value in out_polygon:
590
+ multi_polygon.append(sh_geom.shape(poly))
591
+ except TypeError:
592
+ pass
593
+
594
+ if not multi_polygon:
595
+ print("No polygons generated from raster. Returning None.")
596
+ return None
597
+
598
+ poly = sh_geom.MultiPolygon(multi_polygon) if multi_polygon else None
599
+
600
+ # create GeoDataFrame directly from dictionary
601
+ out_gdata = gpd.GeoDataFrame({"CorriThresh": [corridor_th_value], "geometry": [poly]})
602
+ out_gdata.set_geometry("geometry", inplace=True)
603
+ if shapefile_proj:
604
+ out_gdata = out_gdata.set_crs(shapefile_proj, allow_override=True)
605
+
606
+ if out_gdata is None or out_gdata.empty or out_gdata.geometry.isnull().all():
607
+ print("Empty GeoDataFrame from process_single_footprint.")
608
+ return None
540
609
 
541
610
  return out_gdata
542
611
 
543
612
  except Exception as e:
544
613
  print("Exception: {}".format(e))
545
-
546
- def line_footprint_rel(
547
- in_line,
548
- in_chm,
549
- out_footprint,
550
- processes,
551
- verbose=True,
552
- in_layer=None,
553
- out_layer=None,
554
- max_ln_width=32,
555
- tree_radius=1.5,
556
- max_line_dist=1.5,
557
- canopy_avoidance=0.0,
558
- exponent=1.0,
559
- canopy_thresh_percentage=50,
560
- parallel_mode=bt_const.ParallelMode.MULTIPROCESSING,
561
- ):
562
- """Another version of relative canopy footprint tool."""
563
- footprint = FootprintCanopy(in_line, in_chm, in_layer)
564
- footprint.compute(processes, parallel_mode)
565
-
566
- # footprint.save_line_percentile(out_file_percentile)
567
- footprint.save_footprint(out_footprint, out_layer)
568
-
569
- if __name__ == "__main__":
570
- """This part is to be another version of relative canopy footprint tool."""
571
- in_args, in_verbose = bt_common.check_arguments()
572
- start_time = time.time()
573
- line_footprint_rel(
574
- **in_args.input, processes=int(in_args.processes), verbose=in_verbose
575
- )
576
-
577
- print("Elapsed time: {}".format(time.time() - start_time))
614
+ return None