55
66from tqdm import tqdm
77
8- from pyforestscan .calculate import calculate_fhd , calculate_pad , calculate_pai , assign_voxels , calculate_chm
8+ from pyforestscan .calculate import calculate_fhd , calculate_pad , calculate_pai , assign_voxels , calculate_chm , calculate_canopy_cover
99from pyforestscan .filters import remove_outliers_and_clean
1010from pyforestscan .handlers import create_geotiff
1111from pyforestscan .pipeline import _hag_raster , _hag_delaunay
@@ -50,7 +50,8 @@ def _crop_dtm(dtm_path, tile_min_x, tile_min_y, tile_max_x, tile_max_y):
5050
5151def process_with_tiles (ept_file , tile_size , output_path , metric , voxel_size ,
5252 voxel_height = 1 , buffer_size = 0.1 , srs = None , hag = False ,
53- hag_dtm = False , dtm = None , bounds = None , interpolation = None , remove_outliers = False ) -> None :
53+ hag_dtm = False , dtm = None , bounds = None , interpolation = None , remove_outliers = False ,
54+ cover_min_height : float = 2.0 , cover_k : float = 0.5 ) -> None :
5455 """
5556 Process a large EPT point cloud by tiling, compute CHM or other metrics for each tile,
5657 and write the results to the specified output directory.
@@ -59,7 +60,7 @@ def process_with_tiles(ept_file, tile_size, output_path, metric, voxel_size,
5960 ept_file (str): Path to the EPT file containing the point cloud data.
6061 tile_size (tuple): Size of each tile as (tile_width, tile_height).
6162 output_path (str): Directory where the output files will be saved.
62- metric (str): Metric to compute for each tile ("chm", "fhd", or "pai ").
63+ metric (str): Metric to compute for each tile ("chm", "fhd", "pai", or "cover ").
6364 voxel_size (tuple): Voxel resolution as (x_resolution, y_resolution, z_resolution).
6465 voxel_height (float, optional): Height of each voxel in meters. Required if metric is "pai".
6566 buffer_size (float, optional): Fractional buffer size relative to tile size (e.g., 0.1 for 10% buffer). Defaults to 0.1.
@@ -72,6 +73,8 @@ def process_with_tiles(ept_file, tile_size, output_path, metric, voxel_size,
7273 If None, tiling is done over the entire dataset.
7374 interpolation (str or None, optional): Interpolation method for CHM calculation ("linear", "cubic", "nearest", or None).
7475 remove_outliers (bool, optional): Whether to remove statistical outliers before calculating metrics. Defaults to False.
76+ cover_min_height (float, optional): Height threshold (in meters) for canopy cover (used when metric == "cover"). Defaults to 2.0.
77+ cover_k (float, optional): Beer–Lambert extinction coefficient for canopy cover. Defaults to 0.5.
7578
7679 Returns:
7780 None
@@ -80,7 +83,7 @@ def process_with_tiles(ept_file, tile_size, output_path, metric, voxel_size,
8083 ValueError: If an unsupported metric is requested, if buffer or voxel sizes are invalid, or required arguments are missing.
8184 FileNotFoundError: If the EPT or DTM file does not exist, or a required file for processing is missing.
8285 """
83- if metric not in ["chm" , "fhd" , "pai" ]:
86+ if metric not in ["chm" , "fhd" , "pai" , "cover" ]:
8487 raise ValueError (f"Unsupported metric: { metric } " )
8588
8689 (min_z , max_z ) = (None , None )
@@ -188,7 +191,7 @@ def process_with_tiles(ept_file, tile_size, output_path, metric, voxel_size,
188191
189192 result_file = os .path .join (output_path , f"tile_{ i } _{ j } _chm.tif" )
190193 create_geotiff (chm , result_file , srs , core_extent )
191- elif metric in ["fhd" , "pai" ]:
194+ elif metric in ["fhd" , "pai" , "cover" ]:
192195 voxels , spatial_extent = assign_voxels (tile_points , voxel_size )
193196
194197 if metric == "fhd" :
@@ -204,6 +207,22 @@ def process_with_tiles(ept_file, tile_size, output_path, metric, voxel_size,
204207 else :
205208 result = calculate_pai (pad , voxel_height )
206209 result = np .where (np .isfinite (result ), result , 0 )
210+ elif metric == "cover" :
211+ if not voxel_height :
212+ raise ValueError (f"voxel_height is required for metric { metric } " )
213+
214+ pad = calculate_pad (voxels , voxel_size [- 1 ])
215+ if np .all (pad == 0 ):
216+ result = np .zeros ((pad .shape [0 ], pad .shape [1 ]))
217+ else :
218+ result = calculate_canopy_cover (
219+ pad ,
220+ voxel_height = voxel_height ,
221+ min_height = cover_min_height ,
222+ max_height = None ,
223+ k = cover_k ,
224+ )
225+ result = np .where (np .isfinite (result ), result , 0 )
207226
208227 if current_buffer_size > 0 :
209228 if buffer_pixels_x * 2 >= result .shape [1 ] or buffer_pixels_y * 2 >= result .shape [0 ]:
0 commit comments