Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
e652d1e
Enhance point cloud processing with ept tiling and better error handling
iosefa Nov 29, 2024
5b442e7
Update main.yml
iosefa Nov 29, 2024
f0ccb57
Merge pull request #15 from iosefa/iosefa-patch-1
iosefa Nov 29, 2024
04cd7b3
Refactor documentation structure and remove legacy files
iosefa Nov 30, 2024
36919c7
Add Dockerfile
benleamon Nov 30, 2024
aaaea65
Update Dockerfile
benleamon Nov 30, 2024
ac9e9a6
Update Dockerfile
benleamon Nov 30, 2024
4d71d1f
Add example LAZ data file to documentation
iosefa Nov 30, 2024
b6cb27a
Updated PAI, Added calculate-forest-metrics jupyter notebook
benleamon Nov 30, 2024
82a9d0c
update function for calculating metrics and plotting PAI
benleamon Nov 30, 2024
63fe3e6
Refactor contributing docs and update dependencies
iosefa Nov 30, 2024
cc7637f
Update jupyter notebooks, as well as generate_dtm().
benleamon Nov 30, 2024
a565239
Add optional metric_name for plot colorbar labels
iosefa Dec 1, 2024
7ae8e95
Refactor documentation structure and content
iosefa Dec 1, 2024
5830342
Edited jupyter notebooks
benleamon Dec 1, 2024
799719e
Add new images and documentation files
iosefa Dec 3, 2024
2634fdb
Add CNAME, update dependencies, and CI improvements
iosefa Dec 3, 2024
4667f9e
Merge pull request #16 from iosefa/next
iosefa Dec 3, 2024
14cf768
Enhance CHM interpolation logic and update tests.
iosefa Dec 3, 2024
a82f91e
Merge pull request #17 from iosefa/tests/fit-tests
iosefa Dec 3, 2024
f8ef6bf
Bump version to 0.2.1 in setup.py.
iosefa Dec 3, 2024
cf835bd
Remove unused notebook and update Binder links.
iosefa Dec 3, 2024
0e70339
Add Docker image publishing to workflow
iosefa Dec 3, 2024
e6e4c01
Update conda channel and visualization function names
iosefa Dec 3, 2024
136f774
Add paper.bib and paper.md files for project documentation
iosefa Sep 18, 2024
1d7145e
Add paper edits
benleamon Sep 19, 2024
af62a42
Add acknowledgements section to the paper
iosefa Sep 21, 2024
bd82be2
Update references in Statement of Need section
iosefa Sep 21, 2024
3b90400
Add GitHub Actions workflow for JOSS PDF generation
iosefa Sep 21, 2024
51dafb3
Update workflow to use latest action versions
iosefa Sep 21, 2024
9172804
Replace align with equation for improved LaTeX formatting
iosefa Sep 21, 2024
1ebe013
Added further edits
benleamon Sep 21, 2024
8be74c6
Add spacing between LaTeX equations in the manuscript
iosefa Sep 21, 2024
8785ba0
Fix affiliations and reference typo
iosefa Sep 21, 2024
ada7b5a
Change artifact path for paper upload
iosefa Sep 21, 2024
a279e1a
Add edits refining final paragraph
benleamon Sep 21, 2024
260c9cb
Add affiliation for Benjamin Palsa Leamon
iosefa Sep 21, 2024
5a50849
Fix format for references:
benleamon Sep 29, 2024
ded0c62
Update references and acknowledgements in paper
iosefa Sep 29, 2024
eea729e
Correct typos and standardize term capitalization for #11
iosefa Nov 21, 2024
24b6a23
Add PDAL citation to paper references for #11.
iosefa Nov 21, 2024
16bcad1
Remove author and update paper content to address reviewer comments.
iosefa Nov 27, 2024
caa2925
Update contributions and usage section
benleamon Dec 1, 2024
4ef7f71
Add new references and links, update acknowledgments
iosefa Dec 3, 2024
8e85a28
Merge branch 'docs/joss-paper' into docs/joss-paper-review
iosefa Dec 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ jobs:
test:
runs-on: ubuntu-latest

strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]

steps:
- name: Check out the repository
uses: actions/checkout@v3
Expand All @@ -20,12 +24,12 @@ jobs:
uses: conda-incubator/setup-miniconda@v3
with:
auto-update-conda: true
python-version: "3.10"
python-version: ${{ matrix.python-version }}
channels: conda-forge

- name: Create Conda environment with Python 3.10 and PDAL
- name: Create Conda environment with Python and PDAL
run: |
conda create --name pyforestscan_env python=3.10 pdal gdal -c conda-forge -v
conda create --name pyforestscan_env python=${{ matrix.python-version }} pdal gdal -c conda-forge -v

- name: Activate Conda environment and install dependencies
shell: bash -l {0}
Expand All @@ -38,4 +42,9 @@ jobs:
shell: bash -l {0}
run: |
conda activate pyforestscan_env
pytest
pytest --cov --cov-report=xml

- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
27 changes: 25 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Publish Python Package
name: Publish Python Package and Docker Image

on:
push:
Expand Down Expand Up @@ -28,4 +28,27 @@ jobs:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
run: |
python setup.py sdist bdist_wheel
twine upload dist/* -u __token__ -p $PYPI_TOKEN
twine upload dist/* -u __token__ -p $PYPI_TOKEN

docker:
runs-on: ubuntu-latest
needs: release

steps:
- name: Check out the repository
uses: actions/checkout@v3

- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

- name: Build Docker image
run: docker build -t pyforestscan .

- name: Tag Docker image
run: docker tag pyforestscan iosefa/pyforestscan:latest

- name: Push Docker image
run: docker push iosefa/pyforestscan:latest
11 changes: 8 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,6 @@ instance/
# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/
Expand Down Expand Up @@ -174,3 +171,11 @@ data/

# tests
test_data/output.tif

# tifs created from tests
*.tif

docs/example_data/ept

*.pdf
*.jats
7 changes: 7 additions & 0 deletions .idea/PyForestScan.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 0 additions & 13 deletions .readthedocs.yml

This file was deleted.

28 changes: 28 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
FROM jupyter/base-notebook:latest
LABEL maintainer="Iosefa Percival"
LABEL repo="https://github.com/iosefa/PyForestScan"

USER root
RUN apt-get update -y && apt-get install -y \
gcc g++ make \
libgdal-dev libgl1 sqlite3 && \
apt-get clean && rm -rf /var/lib/apt/lists/*

RUN fix-permissions "${CONDA_DIR}" && \
fix-permissions "/home/${NB_USER}"

USER 1000
RUN mamba install -c conda-forge sqlite gdal pdal -y && \
pip install --no-cache-dir pyforestscan jupyter-server-proxy && \
mamba update -c conda-forge -y && \
jupyter server extension enable --sys-prefix jupyter_server_proxy

RUN mkdir ./examples
COPY /docs/examples ./examples

ENV PROJ_LIB='/opt/conda/share/proj'
ENV JUPYTER_ENABLE_LAB=yes

USER root
RUN chown -R ${NB_UID} ${HOME}
USER ${NB_USER}
38 changes: 26 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
# PyForestScan: Airborne Point Cloud Analysis for Forest Structure

[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/iosefa/PyForestScan/HEAD?labpath=docs%2Fexamples%2Fgetting-started-importing-preprocessing-dtm-chm.ipynb)
[![PyPI](https://img.shields.io/pypi/v/PyForestScan.svg)](https://pypi.org/project/PyForestScan/)
[![Docker Pulls](https://img.shields.io/docker/pulls/iosefa/pyforestscan?logo=docker&label=pulls)](https://hub.docker.com/r/iosefa/pyforestscan)
[![Contributors](https://img.shields.io/github/contributors/iosefa/PyForestScan.svg?label=contributors)](https://github.com/iosefa/PyForestScan/graphs/contributors)
[![Tests](https://img.shields.io/github/actions/workflow/status/iosefa/PyForestScan/main.yml?branch=main)](https://github.com/iosefa/PyForestScan/actions/workflows/main.yml)
[![Coverage](https://img.shields.io/codecov/c/github/iosefa/PyForestScan/main)](https://codecov.io/gh/iosefa/PyForestScan)

**Calculate Forest Structural Metrics from lidar point clouds in Python**

![Height Above Ground](./screenshots/hag.png)

## Overview

PyForestScan is a Python library designed for analyzing and visualizing forest structure using airborne
3D point cloud data. The library helps derive important forest metrics such as Canopy Height,
Plant Area Index (PAI), Canopy Cover, Plant Area Density (PAD), and Foliage Height Diversity (FHD).
PyForestScan is a Python library designed for analyzing and visualizing forest structure using airborne 3D point cloud data. The library helps derive important forest metrics such as Canopy Height, Plant Area Index (PAI), Canopy Cover, Plant Area Density (PAD), and Foliage Height Diversity (FHD).

## Features

- **Forest Metrics**: Calculate and visualize key metrics like Canopy Height, PAI, PAD, and FHD.
- **Airborne Data Compatibility**: Supports LiDAR and Structure from Motion (SfM) data from drones and UAVs.
- **Visualization**: Create 2D and 3D visualizations of forest structures.
- **Large Point Cloud Support**: Utilizes efficient data formats such as EPT for large point cloud processing.
- **Visualization**: Create 2D and 3D visualizations of forest structure and structural metrics
- **Extensibility**: Easily add custom filters and visualization techniques to suit your needs.

## Installation
Expand All @@ -26,27 +33,34 @@ pip install pyforestscan
### Dependencies

> [!IMPORTANT]
> You MUST have installed PDAL to use PyForestScan. If you use conda to install PDAL, make sure you install pyforestscan in the conda environment with PDAL. See https://pdal.io/en/latest/ for more information.
> You MUST have installed both PDAL and GDAL to use PyForestScan. If you use conda to install PDAL, make sure you install pyforestscan in the conda environment with PDAL (and GDAL if using conda). See https://pdal.io/en/latest/ for more information on PDAL and https://gdal.org/en/stable/.

- PDAL >= 2.7
- GDAL >= 3.5
- Python >= 3.10

## Quick Start

### Derive Forest Metrics from Airborne Data
### Calculate, Export, and Plot Plant Area Index

The following snippet shows how you can load a las file, create 5m by 5m by 1m voxels with points assigned to them, and generate plant area density at 1m layers and plant area index for each 5m grid cell before writing the resulting PAI layer to a geotiff and plotting.

The following snipped shows how you can load a las file, create 25m by 25m by 5m voxels with points assigned to them, and generate plant area density at 5m layers and plant area index for each 25m grid cell before writing the resulting PAI layer to a geotiff.
```python
from pyforestscan.handlers import read_lidar, create_geotiff
from pyforestscan.calculate import assign_voxels, calculate_pad, calculate_pai
from pyforestscan.visualize import plot_metric

arrays = read_lidar("path/to/lidar/file.las", "EPSG:32605", hag=True)
voxels, extent = assign_voxels(arrays[0], (25, 25, 5))
pad = calculate_pad(voxels, 5)
arrays = read_lidar("example_data/20191210_5QKB020880.laz", "EPSG:32605", hag=True)
voxel_resolution = (5, 5, 1)
voxels, extent = assign_voxels(arrays[0], voxel_resolution)
pad = calculate_pad(voxels, voxel_resolution[-1])
pai = calculate_pai(pad)
create_geotiff(pai, "output_pai.tiff", "EPSG:32605", extent)
plot_metric('Plant Area Index', pai, extent, metric_name='PAI', cmap='viridis', fig_size=None)
```

![Plant Area Index](./screenshots/pai.png)

## Documentation

For detailed instructions and examples, visit our [documentation](https://pyforestscan.readthedocs.io/).
Expand Down Expand Up @@ -85,7 +99,7 @@ pytest tests/test_calculate.py

## Contributing

We welcome contributions! Please check our [Contributing Guidelines](CONTRIBUTING.md) to get started.
We welcome contributions! Please check our [Contributing Guidelines](docs/contributing.md) to get started.

## License

Expand Down
1 change: 1 addition & 0 deletions docs/CNAME
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pyforestscan.sefa.ai
20 changes: 0 additions & 20 deletions docs/Makefile

This file was deleted.

3 changes: 3 additions & 0 deletions docs/api/calculate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Calculate Module

::: pyforestscan.calculate
3 changes: 3 additions & 0 deletions docs/api/filters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Filters Module

::: pyforestscan.filters
3 changes: 3 additions & 0 deletions docs/api/handlers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Handlers Module

::: pyforestscan.handlers
3 changes: 3 additions & 0 deletions docs/api/pipeline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Pipeline Module

::: pyforestscan.pipeline
3 changes: 3 additions & 0 deletions docs/api/process.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Process Module

::: pyforestscan.process
3 changes: 3 additions & 0 deletions docs/api/visualize.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Visualize Module

::: pyforestscan.visualize
71 changes: 71 additions & 0 deletions docs/benchmarks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Benchmarking

PyForestScan is designed for high performance and memory efficiency, ensuring it can handle large-scale point cloud datasets effectively. While no other Python libraries specifically calculate these forest structure metrics, there are alternatives in R, such as the`leafR` library (Almeida et al. 2021), that offer similar functionality.

We provide a direct performance comparison between PyForestScan and`leafR` to demonstrate its efficiency. In both cases, we calculate Plant Area Index (PAI) (this is labelled as Leaf Area Index in the `leafR` library) on a LAS tile, repeating the process 100 times and plotting the results. The measured benchmarking is only done on the functions to calculate PAI/LAI. It does not include time taken to load the point cloud, etc.


The benchmarks were conducted on a Mac with an Apple M3 Max processor (16 cores) and 128GB RAM.

![Benchmark comparison between PyForestScan and leafR](images/lai_computation_times.png)

## Code Used
To calculate LAI in `leafR`, and compare these with the python code, we:

```R
# Install and load required packages
if (!require("lidR")) install.packages("lidR")
if (!require("raster")) install.packages("raster")
if (!require("leafR")) install.packages("leafR")

library(lidR)
library(raster)
library(leafR)

file_path <- "example_data/20191126_5QKB020840_normalized.laz"
las <- readLAS(file_path)
if (is.empty(las)) {
stop("The LAS file is empty or could not be read.")
}

if (is.null(las@data$Z)) {
stop("The LAS file does not contain Z coordinates.")
}

if (!"Zref" %in% names(las@data)) {
las <- normalize_height(las, tin())
}

temp_las_file <- tempfile(fileext = ".las")
writeLAS(las, temp_las_file)

compute_lai <- function(las_file_path) {
lad_voxels <- lad.voxels(las_file_path, grain.size = 25)
lai_raster <- lai.raster(lad_voxels)
return(lai_raster)
}

timing_results <- data.frame(software = character(),
time = numeric(),
stringsAsFactors = FALSE)

for (i in 1:100) {
start_time <- Sys.time()
lai_result <- compute_lai(temp_las_file)
end_time <- Sys.time()
iteration_time <- as.numeric(difftime(end_time, start_time, units = "secs"))

timing_results <- rbind(timing_results, data.frame(software = "R::leafR", time = iteration_time))
cat(sprintf("Iteration %d completed in %.2f seconds.\n", i, iteration_time))
}

write.csv(timing_results, file = "timing_results.csv", row.names = FALSE)

cat(sprintf("Total time for 100 iterations: %.2f seconds.\n", sum(timing_results$time)))

unlink(temp_las_file)
```

## Reference

Almeida, Danilo Roberti Alves de, Scott Christopher Stark, Carlos Alberto Silva, Caio Hamamura, and Ruben Valbuena. 2021. "leafR: Calculates the Leaf Area Index (LAD) and Other Related Functions."Manual. <https://CRAN.R-project.org/package=leafR>.
Loading
Loading