| Title: | Gap Light Analysis of Virtual and Real Fisheye Photography |
|---|---|
| Description: | Analyzes forest canopy gap light transmission using LiDAR point cloud data and hemispherical photography. Provides tools for processing LiDAR data, computing horizon angles, and calculating gap light metrics. |
| Authors: | Gord Frazer [aut] (Conceptualization and original code), Sam Albers [aut, cre], Tula Foundation [fnd, cph], Fisheries and Oceans Canada [fnd], Province of British Columbia [fnd], Nanwakolas Council [fnd] |
| Maintainer: | Sam Albers <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.0.0.9000 |
| Built: | 2026-05-30 09:12:39 UTC |
| Source: | https://github.com/HakaiInstitute/gaplightr |
Batch process LiDAR data to create synthetic hemispherical (fisheye) photographs for multiple spatial points. Supports parallel processing and can resume from previous runs.
gla_create_fisheye_photos( points, output_dir, camera_height_m = 1.37, min_dist = 1, img_res = 2800, max_cex = 1.5, min_cex = 0.04, pointsize = 20, dpi = 1200, parallel = TRUE, resume = TRUE, radial_distortion = "equidistant" )gla_create_fisheye_photos( points, output_dir, camera_height_m = 1.37, min_dist = 1, img_res = 2800, max_cex = 1.5, min_cex = 0.04, pointsize = 20, dpi = 1200, parallel = TRUE, resume = TRUE, radial_distortion = "equidistant" )
points |
An sf object containing spatial points with required columns:
|
output_dir |
Directory path where fisheye photo BMP files will be saved |
camera_height_m |
Camera height above ground in meters. Default is 1.37m |
min_dist |
Minimum distance from camera to include LiDAR points (meters).
Points closer than this distance are excluded. Must be greater than 0 -
a value of 0 would allow rho = 0, causing division by zero in point size
scaling. Because points with rho < min_dist are filtered out in
|
img_res |
Image resolution in pixels (width and height). Default is 2800. |
max_cex |
Maximum symbol size for plotting points (CEX value). Controls the size of points at rho = 1 (1 metre from camera) under the inverse-distance formula. Default is 1.5. |
min_cex |
Minimum symbol size for plotting points (CEX value). The asymptotic lower bound approached as distance increases. Default is 0.04. |
pointsize |
Point size parameter for bitmap graphics device. Default is 20. |
dpi |
Resolution in dots per inch for output image. Default is 1200. |
parallel |
Logical. If TRUE (default), use parallel processing via
|
resume |
Logical. If TRUE (default), skip points that already have fisheye photos in the output directory |
radial_distortion |
Lens projection method. Use "equidistant" (default)
for standard equidistant polar projection, or provide custom lens calibration
data (see |
This function processes multiple points in batch, transforming LiDAR data into synthetic hemispherical photographs. For each point, it:
Reads and transforms the LiDAR point cloud
Projects points onto a hemispherical image plane
Creates a bitmap image with distance-weighted point sizes
Saves the result as a BMP file
Graphical parameter calibration: The default values for img_res,
pointsize, min_cex, max_cex, and dpi were derived from calibration
against real hemispherical photographs collected in a coastal temperate
rainforest. Because optimal values depend on forest structure, canopy
density, and LiDAR point density, users working in other forest types or
with different LiDAR acquisitions should expect to calibrate these parameters
for their study region. Calibration can be done formally (e.g., optimizing
against co-located real photos) or informally through visual trial and error.
Parallel processing can significantly speed up processing for many points.
Set up a parallel plan before calling this function:
future::plan(future::multisession, workers = 4)
The resume feature allows you to interrupt and restart processing without re-creating existing photos.
The input points sf object with an added column
fisheye_photo_path containing the file paths to the generated
fisheye photos
gla_extract_horizons() for extracting horizon masks
## Not run: # Assuming you have points with horizon masks already extracted future::plan(future::multisession, workers = 4) points_with_photos <- gla_create_fisheye_photos( points = stream_points, output_dir = "output/fisheye_photos", camera_height_m = 1.37, parallel = TRUE, resume = TRUE ) ## End(Not run)## Not run: # Assuming you have points with horizon masks already extracted future::plan(future::multisession, workers = 4) points_with_photos <- gla_create_fisheye_photos( points = stream_points, output_dir = "output/fisheye_photos", camera_height_m = 1.37, parallel = TRUE, resume = TRUE ) ## End(Not run)
Clips circular plots from a LiDAR catalog at specified point locations. Processes points in batches to avoid memory exhaustion with large datasets.
gla_create_virtual_plots( points, folder, output_dir, plot_radius, filter = "-keep_class 1 2 9", chunk_size = 0, batch_size = 1000, resume = TRUE, check_las_catalog = FALSE, ... )gla_create_virtual_plots( points, folder, output_dir, plot_radius, filter = "-keep_class 1 2 9", chunk_size = 0, batch_size = 1000, resume = TRUE, check_las_catalog = FALSE, ... )
points |
sf object with point locations |
folder |
Directory containing LAS/LAZ files |
output_dir |
Directory to save clipped plot files |
plot_radius |
Radius of circular plots in meters |
filter |
LAS filter string (default: "-keep_class 1 2 9") |
chunk_size |
Chunk size for LAScatalog processing (default: 0) |
batch_size |
Number of plots to process per batch (default: 1000). Reduce if encountering memory errors with large datasets. |
resume |
Skip points with existing output files (default: TRUE) |
check_las_catalog |
Run las_check on catalog (default: FALSE) |
... |
Additional arguments passed to readLAScatalog |
Processes plots in batches to prevent memory exhaustion. With large datasets (e.g., 5000+ plots), processing all at once can fail. The batch_size parameter controls how many plots are clipped simultaneously. Default of 1000 works well for most systems. Reduce to 500 or lower if still encountering memory issues.
sf object with added las_files column containing paths to clipped plots
points_path <- system.file("extdata", "points.geojson", package = "gaplightr") dem_path <- system.file("extdata", "dem.tif", package = "gaplightr") las_folder <- system.file("extdata", "lidar", package = "gaplightr") points <- gla_load_points(points_path, dem_path) points <- gla_create_virtual_plots( points = points, folder = las_folder, output_dir = tempdir(), plot_radius = 50 )points_path <- system.file("extdata", "points.geojson", package = "gaplightr") dem_path <- system.file("extdata", "dem.tif", package = "gaplightr") las_folder <- system.file("extdata", "lidar", package = "gaplightr") points <- gla_load_points(points_path, dem_path) points <- gla_create_virtual_plots( points = points, folder = las_folder, output_dir = tempdir(), plot_radius = 50 )
Processes a fisheye image and computes gap fraction (proportion of sky visible) in elevation and azimuth bins.
gla_extract_gap_fraction( img_file, elev_res = 5, azi_res = 5, rotation_deg = 0, radial_distortion = "equidistant", threshold = 0 )gla_extract_gap_fraction( img_file, elev_res = 5, azi_res = 5, rotation_deg = 0, radial_distortion = "equidistant", threshold = 0 )
img_file |
Path to fisheye image file |
elev_res |
Elevation resolution in degrees (default 5) |
azi_res |
Azimuth resolution in degrees (default 5) |
rotation_deg |
Rotation angle in degrees to align image with true north (default 0) |
radial_distortion |
Lens projection method. Use "equidistant" (default)
for standard equidistant polar projection, or provide custom lens calibration
data (see |
threshold |
Threshold value for converting image to binary. Can be:
|
A list with gap fraction matrix and metadata
photo <- system.file("extdata", "example-photo.jpg", package = "gaplightr") gla_extract_gap_fraction(photo) # Use lens calibration sigma_cal <- gla_lens_sigma_8mm() gla_extract_gap_fraction(photo, radial_distortion = sigma_cal)photo <- system.file("extdata", "example-photo.jpg", package = "gaplightr") gla_extract_gap_fraction(photo) # Use lens calibration sigma_cal <- gla_lens_sigma_8mm() gla_extract_gap_fraction(photo, radial_distortion = sigma_cal)
Extracts terrain horizon elevation angles from a DEM for multiple observation points and converts them to horizon masks suitable for fisheye photo creation. Supports caching to disk for efficient resume of interrupted workflows.
gla_extract_horizons( points, dem_path, output_dir, step = 5, max_search_distance = NULL, distance_step = NULL, camera_height_m = 1.37, parallel = TRUE, resume = TRUE, verbose = FALSE )gla_extract_horizons( points, dem_path, output_dir, step = 5, max_search_distance = NULL, distance_step = NULL, camera_height_m = 1.37, parallel = TRUE, resume = TRUE, verbose = FALSE )
points |
sf object containing observation points with point geometry. Must have x_meters and y_meters columns. Coordinates will be extracted from the geometry and transformed to WGS84 if necessary. |
dem_path |
Character path to the DEM raster file (GeoTIFF) |
output_dir |
Character path to directory for caching horizon CSV files. Each point's horizon data is saved as x_y_horizon.csv. Required parameter. |
step |
Numeric azimuth step size in degrees for horizon calculation (default: 5) |
max_search_distance |
Maximum search distance in meters for horizon detection (default: NULL, uses full DEM extent) |
distance_step |
Distance step size in meters for sampling along line of sight (default: NULL, uses raster resolution) |
camera_height_m |
Camera height above ground in meters. The observer elevation is calculated as ground elevation (from DEM) plus this height. |
parallel |
Logical. If TRUE (default), use parallel processing via
|
resume |
Logical indicating whether to skip points that already have cached horizon files (default: TRUE). When FALSE, recomputes all horizons. |
verbose |
Logical indicating whether to print progress messages (default: FALSE) |
When parallel = TRUE, each worker loads the DEM independently from disk.
Set up a parallel plan before calling this function:
future::plan(future::multisession, workers = 3)
The input sf object with an added horizon_mask list-column. Each element is a list containing:
Numeric vector of x-coordinates for horizon mask polygon
Numeric vector of y-coordinates for horizon mask polygon
gla_create_fisheye_photos() for using extracted horizons
## Not run: points <- gla_load_points("points.gpkg", "dem.tif") points <- gla_extract_horizons( points = points, dem_path = "dem.tif", output_dir = "output/horizons" ) ## End(Not run)## Not run: points <- gla_load_points("points.gpkg", "dem.tif") points <- gla_extract_horizons( points = points, dem_path = "dem.tif", output_dir = "output/horizons" ) ## End(Not run)
Returns the radial distortion calibration data for a Sigma 8mm f/3.5 EX DG circular fisheye lens. This calibration maps normalized image radius to elevation angle.
gla_lens_sigma_8mm()gla_lens_sigma_8mm()
A list with two components:
radius |
Normalized radial distance from image center (0 to 1) |
elevation |
Elevation angle in radians (0 at horizon, pi/2 at zenith) |
A list with components radius (normalized image radius),
elevation (elevation angle in radians), and name (lens
identifier for this calibration; currently "sigma8mm").
Loads point locations from a spatial file or sf object, validates them against a DEM, and enriches each point with elevation, projected coordinates, and WGS84 lat/lon. The result is the starting point for all downstream gaplightr workflows.
gla_load_points(x, dem, drop_na_dem = FALSE, ...)gla_load_points(x, dem, drop_na_dem = FALSE, ...)
x |
Either a file path to any file that |
dem |
Either a file path to a DEM raster file, or a |
drop_na_dem |
Logical. If |
... |
Additional arguments passed to |
This function performs strict validation:
Geometry type must be POINT
CRS must be defined and projected (not geographic lat/lon)
Point and DEM CRS must match exactly
All points must fall within DEM spatial extent
Points on NoData cells error by default; set drop_na_dem = TRUE to
drop them with a warning instead
Every point is assigned a point_id, a positive integer used to name all
downstream output files (LAS clips, horizon CSVs, fisheye photos). If x
does not contain a point_id column, sequential IDs are assigned
automatically (1, 2, 3, ...). To use your own IDs (for example to preserve
cached outputs across re-runs, or to match an existing site numbering scheme),
include a point_id column containing unique positive integers before
calling this function.
points_path <- system.file("extdata", "points.geojson", package = "gaplightr") dem_path <- system.file("extdata", "dem.tif", package = "gaplightr") points <- gla_load_points(points_path, dem_path) # Supply your own point_id values to keep cached outputs stable across # re-runs or to match an existing site numbering scheme. dem_path <- system.file("extdata", "dem.tif", package = "gaplightr") my_points <- sf::st_read( system.file("extdata", "points.geojson", package = "gaplightr"), quiet = TRUE ) my_points$point_id <- c(101L, 202L, 303L) # user-defined IDs points <- gla_load_points(my_points, dem_path)points_path <- system.file("extdata", "points.geojson", package = "gaplightr") dem_path <- system.file("extdata", "dem.tif", package = "gaplightr") points <- gla_load_points(points_path, dem_path) # Supply your own point_id values to keep cached outputs stable across # re-runs or to match an existing site numbering scheme. dem_path <- system.file("extdata", "dem.tif", package = "gaplightr") my_points <- sf::st_read( system.file("extdata", "points.geojson", package = "gaplightr"), quiet = TRUE ) my_points$point_id <- c(101L, 202L, 303L) # user-defined IDs points <- gla_load_points(my_points, dem_path)
Batch processes fisheye photos for multiple points, computing gap fractions and transmitted solar radiation. This is the main workflow function for analyzing hemispherical fisheye photos.
gla_process_fisheye_photos( points, clearsky_coef = 0.65, time_step_min = 2, day_start = 1, day_end = 365, day_res = 1, elev_res = 5, azi_res = 5, Kt = 0.45, rotation_deg = 0, parallel = TRUE, keep_gap_fraction_data = FALSE, radial_distortion = "equidistant", threshold = 0, solar_constant = 1367 )gla_process_fisheye_photos( points, clearsky_coef = 0.65, time_step_min = 2, day_start = 1, day_end = 365, day_res = 1, elev_res = 5, azi_res = 5, Kt = 0.45, rotation_deg = 0, parallel = TRUE, keep_gap_fraction_data = FALSE, radial_distortion = "equidistant", threshold = 0, solar_constant = 1367 )
points |
An sf object with point locations. Must contain columns:
|
clearsky_coef |
Clear-sky transmission coefficient (default 0.65). Proportion of extraterrestrial radiation reaching the surface under clear skies. |
time_step_min |
Time step in minutes for computing solar positions (default 2). |
day_start |
Start day of year (default 1). Use day-of-year format (1-365). |
day_end |
End day of year (default 365). |
day_res |
Day resolution - compute every N days (default 1 for daily). |
elev_res |
Elevation resolution in degrees (default 5) |
azi_res |
Azimuth resolution in degrees (default 5) |
Kt |
Mean cloudiness index (Kt = H/Ho) for the period of interest (default 0.45). Ratio of measured to extraterrestrial radiation. |
rotation_deg |
Rotation angle in degrees to align image with true north (default 0) |
parallel |
Logical. If TRUE (default), use parallel processing via
|
keep_gap_fraction_data |
Include gap fraction matrix in output (default FALSE).
If TRUE, adds |
radial_distortion |
Lens projection method. Use "equidistant" (default)
for standard equidistant polar projection, or provide custom lens calibration
data (see |
threshold |
Threshold value for converting image to binary. Can be:
|
solar_constant |
Solar constant in W/m² (default 1367). The total solar electromagnetic radiation per unit area at the top of Earth's atmosphere. |
An sf object with computed solar radiation metrics:
canopy_openness_pct |
Canopy openness percentage derived from the fisheye photo gap fraction |
mean_daily_extraterrestrial_irradiance_Wm2 |
Mean daily irradiance above the atmosphere (W/m²). Computed from astronomical parameters only - no atmospheric, canopy, or topographic effects. |
mean_daily_direct_irradiation_MJm2d |
Mean daily direct irradiation on a flat open surface (MJ/m²/day).
Computed from extraterrestrial irradiance and |
mean_daily_diffuse_irradiation_MJm2d |
Mean daily diffuse irradiation on a flat open surface (MJ/m²/day).
Computed from extraterrestrial irradiance and |
mean_daily_global_irradiation_MJm2d |
Mean daily global irradiation on a flat open surface (MJ/m²/day).
Computed from extraterrestrial irradiance and |
transmitted_direct_irradiation_MJm2d |
Direct irradiation reaching the sensor (MJ/m²/day), accounting for both canopy and topographic shading encoded in the fisheye photo. |
transmitted_diffuse_irradiation_MJm2d |
Diffuse irradiation reaching the sensor (MJ/m²/day), accounting for both canopy and topographic shading encoded in the fisheye photo. |
transmitted_global_irradiation_MJm2d |
Global irradiation reaching the sensor (MJ/m²/day), accounting for both canopy and topographic shading encoded in the fisheye photo. |
transmitted_direct_irradiation_pct |
Transmitted direct irradiation as a percentage of the flat
open-sky reference ( |
transmitted_diffuse_irradiation_pct |
Transmitted diffuse irradiation as a percentage of the flat
open-sky reference ( |
transmitted_global_irradiation_pct |
Transmitted global irradiation as a percentage of the flat
open-sky reference ( |
## Not run: # Process fisheye photos for August (days 213-243) results <- gla_process_fisheye_photos( points = stream_points, day_start = 213, day_end = 243, Kt = 0.45 ) # Use automatic thresholding for real photos results <- gla_process_fisheye_photos( points = stream_points, threshold = "auto" ) # Keep gap fraction data for further analysis results <- gla_process_fisheye_photos( points = stream_points, keep_gap_fraction_data = TRUE ) ## End(Not run)## Not run: # Process fisheye photos for August (days 213-243) results <- gla_process_fisheye_photos( points = stream_points, day_start = 213, day_end = 243, Kt = 0.45 ) # Use automatic thresholding for real photos results <- gla_process_fisheye_photos( points = stream_points, threshold = "auto" ) # Keep gap fraction data for further analysis results <- gla_process_fisheye_photos( points = stream_points, keep_gap_fraction_data = TRUE ) ## End(Not run)