Tools to estimate the Canopy Openness Index (COI) from
hemispherical (fisheye) canopy photographs. The package imports canopy
images, crops them to the circular area delimited by the fisheye lens,
binarizes them to separate sky from vegetation pixels, and computes the
index as the proportion of sky pixels in the cropped image. Canopy
openness is a measure related to canopy closure, canopy cover and
understorey illumination, as discussed in Jennings, Brown and Sheil
(1999) "Assessing forest canopies and understorey illumination: canopy
closure, canopy cover and other measures"
Edson Silva-Júnior 2026-09-29

Package to calculate Canopy Openness Index (COI) from canopy images
require("devtools")
devtools::install_github("Edbbioeco/coiR")
coiR package is downloaded now, so we can library it. Additionally, we
library the following packages:
terra: import images files as rasters;
purrr: create loops for multiple operations;
ggplot2: create elegant graphs for imported images;
tidyterra: visualize RGB raster
library(coiR)
library(terra)
library(purrr)
library(ggplot2)
library(tidyterra)
Now, we need to import our data Images may be shotten photos, as .png,
.jpg or .jpeg files. first, we import package exemple data, using
coiR::coir_data() function, and import them using terra::rast()
function for every image, throught a loop with purrr::map() function.
Our images (images) are setted as a list class object.
images <- purrr::map(coiR::data_coir(),
~.x |> terra::rast())
images
[[1]]
class : SpatRaster
size : 800, 800, 4 (nrow, ncol, nlyr)
resolution : 1, 1 (x, y)
extent : 0, 800, 0, 800 (xmin, xmax, ymin, ymax)
coord. ref. :
source : imagem1.png
names : imagem1_1, imagem1_2, imagem1_3, imagem1_4
[[2]]
class : SpatRaster
size : 800, 800, 4 (nrow, ncol, nlyr)
resolution : 1, 1 (x, y)
extent : 0, 800, 0, 800 (xmin, xmax, ymin, ymax)
coord. ref. :
source : imagem2.png
names : imagem2_1, imagem2_2, imagem2_3, imagem2_4
[[3]]
class : SpatRaster
size : 800, 800, 4 (nrow, ncol, nlyr)
resolution : 1, 1 (x, y)
extent : 0, 800, 0, 800 (xmin, xmax, ymin, ymax)
coord. ref. :
source : imagem3.png
names : imagem3_1, imagem3_2, imagem3_3, imagem3_4
[[4]]
class : SpatRaster
size : 800, 800, 4 (nrow, ncol, nlyr)
resolution : 1, 1 (x, y)
extent : 0, 800, 0, 800 (xmin, xmax, ymin, ymax)
coord. ref. :
source : imagem4.png
names : imagem4_1, imagem4_2, imagem4_3, imagem4_4
Next, lets visualize every image, using a purrr::map() loop, through
ggplot and tidyterra::geom_spatraster_rgb() function.
purrr::map(images,
purrr::in_parallel(
~ggplot() +
tidyterra::geom_spatraster_rgb(data = .x) +
theme_void()
),
.progress = TRUE)
[[1]]

[[2]]

[[3]]

[[4]]

For our exemples, lets work in two ways: run for a single and run for multiple images. Lets set first image as a single image.
single_image <- images[[1]]
Usely, canopy images are shotten photos, square images. For our
analysis, we need to crop images into circles. We use
coiR::coir_crop() |function.
single_image |>
coiR::coir_crop()

class : SpatRaster
size : 800, 800, 4 (nrow, ncol, nlyr)
resolution : 1, 1 (x, y)
extent : 0, 800, 0, 800 (xmin, xmax, ymin, ymax)
coord. ref. :
source(s) : memory
varname : imagem1
names : imagem1_1, imagem1_2, imagem1_3, imagem1_4
min values : 4, 25, 33, 29
max values : 243, 241, 246, 255
And we also can analyse multiple images from an one shot, using
purrr::map() loop.
purrr::map(images, coiR::coir_crop)




[[1]]
class : SpatRaster
size : 800, 800, 4 (nrow, ncol, nlyr)
resolution : 1, 1 (x, y)
extent : 0, 800, 0, 800 (xmin, xmax, ymin, ymax)
coord. ref. :
source(s) : memory
varname : imagem1
names : imagem1_1, imagem1_2, imagem1_3, imagem1_4
min values : 4, 25, 33, 29
max values : 243, 241, 246, 255
[[2]]
class : SpatRaster
size : 800, 800, 4 (nrow, ncol, nlyr)
resolution : 1, 1 (x, y)
extent : 0, 800, 0, 800 (xmin, xmax, ymin, ymax)
coord. ref. :
source(s) : memory
varname : imagem2
names : imagem2_1, imagem2_2, imagem2_3, imagem2_4
min values : 2, 21, 21, 29
max values : 255, 255, 255, 255
[[3]]
class : SpatRaster
size : 800, 800, 4 (nrow, ncol, nlyr)
resolution : 1, 1 (x, y)
extent : 0, 800, 0, 800 (xmin, xmax, ymin, ymax)
coord. ref. :
source(s) : memory
varname : imagem3
names : imagem3_1, imagem3_2, imagem3_3, imagem3_4
min values : 3, 19, 7, 29
max values : 255, 255, 255, 255
[[4]]
class : SpatRaster
size : 800, 800, 4 (nrow, ncol, nlyr)
resolution : 1, 1 (x, y)
extent : 0, 800, 0, 800 (xmin, xmax, ymin, ymax)
coord. ref. :
source(s) : memory
varname : imagem4
names : imagem4_1, imagem4_2, imagem4_3, imagem4_4
min values : 1, 13, 1, 30
max values : 255, 255, 255, 255
Our next step is to binarize our images. We use coiR::coir_binarize()
function. To avoid replot canopy image, we use plot = FALSE argument
at coiR::coir_crop() function. To facilite our analysis, lets use pipe
(|>) to conect functions output.
single_image |>
coiR::coir_crop(plot = FALSE) |>
coiR::coir_binarize()

class : SpatRaster
size : 800, 800, 1 (nrow, ncol, nlyr)
resolution : 1, 1 (x, y)
extent : 0, 800, 0, 800 (xmin, xmax, ymin, ymax)
coord. ref. :
source(s) : memory
name : variavel
min value : 0
max value : 1
As previously made, we can binarize multiple images, making a function
in our purrr::map() loop.
purrr::map(images,
purrr::in_parallel(
~coiR::coir_crop(data = .x,
plot = FALSE) |>
coiR::coir_binarize()
),
.progress = TRUE)




[[1]]
class : SpatRaster
size : 800, 800, 1 (nrow, ncol, nlyr)
resolution : 1, 1 (x, y)
extent : 0, 800, 0, 800 (xmin, xmax, ymin, ymax)
coord. ref. :
source(s) : memory
name : variavel
min value : 0
max value : 1
[[2]]
class : SpatRaster
size : 800, 800, 1 (nrow, ncol, nlyr)
resolution : 1, 1 (x, y)
extent : 0, 800, 0, 800 (xmin, xmax, ymin, ymax)
coord. ref. :
source(s) : memory
name : variavel
min value : 0
max value : 1
[[3]]
class : SpatRaster
size : 800, 800, 1 (nrow, ncol, nlyr)
resolution : 1, 1 (x, y)
extent : 0, 800, 0, 800 (xmin, xmax, ymin, ymax)
coord. ref. :
source(s) : memory
name : variavel
min value : 0
max value : 1
[[4]]
class : SpatRaster
size : 800, 800, 1 (nrow, ncol, nlyr)
resolution : 1, 1 (x, y)
extent : 0, 800, 0, 800 (xmin, xmax, ymin, ymax)
coord. ref. :
source(s) : memory
name : variavel
min value : 0
max value : 1
Finally, we get COI index. We use coiR::coir_index() for get that
value. To avoid replot cropped and binarized images, we set
plot = FALSE for both coiR::coir_crop() and coiR::coir_binarize()
functions.
single_image |>
coiR::coir_crop(plot = FALSE) |>
coiR::coir_binarize(plot = FALSE) |>
coiR::coir_index()
[1] 0.5
As previously made, we can also do for multiple images, making a
function in purrr::map_dbl() function.
purrr::map_dbl(images,
purrr::in_parallel(
~coiR::coir_crop(data = .x,
plot = FALSE) |>
coiR::coir_binarize(plot = FALSE) |>
coiR::coir_index()
),
.progress = TRUE)
[1] 0.50 0.52 0.28 0.29
Finally, we make a data rame with those values, using
purrr::imap_dfr(), to get image names.
df_index <- purrr::imap_dfr(
images,
purrr::in_parallel(
\(imagem, nome){
index <- imagem |>
coiR::coir_crop(plot = FALSE) |>
coiR::coir_binarize(plot = FALSE) |>
coiR::coir_index(round = 3)
tibble::tibble(id = nome,
Index = index)
}
),
.progress = TRUE)
df_index
# A tibble: 4 × 2
id Index
<int> <dbl>
1 1 0.501
2 2 0.518
3 3 0.281
4 4 0.29