| Title: | CAnopy IMage ANalysis |
|---|---|
| Description: | Tools for preprocessing and analysis of canopy photographs. |
| Authors: | Gastón Mauro Díaz [aut, cre] (ORCID: <https://orcid.org/0000-0002-0362-8616>) |
| Maintainer: | Gastón Mauro Díaz <[email protected]> |
| License: | GPL-3 |
| Version: | 2.1.3.9000 |
| Built: | 2026-05-12 08:48:42 UTC |
| Source: | https://github.com/gastonmaurodiaz/rcaiman |
Applies a method to each set of pixels defined by a direction and a constant
field of view (FOV). By default, several built-in methods are available
(see method), but a custom function can also be provided via the fun
argument.
apply_by_direction( r, z, a, m, spacing = 10, laxity = 2.5, fov = c(30, 40, 50), method = c("thr_isodata", "thr_twocorner", "detect_bg_dn", "fit_coneshaped_model", "fit_trend_surface_np1", "fit_trend_surface_np6"), fun = NULL, parallel = TRUE, cores = NULL, logical = TRUE, leave_free = 1 )apply_by_direction( r, z, a, m, spacing = 10, laxity = 2.5, fov = c(30, 40, 50), method = c("thr_isodata", "thr_twocorner", "detect_bg_dn", "fit_coneshaped_model", "fit_trend_surface_np1", "fit_trend_surface_np6"), fun = NULL, parallel = TRUE, cores = NULL, logical = TRUE, leave_free = 1 )
r |
terra::SpatRaster of one or more layers (e.g., RGB channels or binary masks) in fisheye projection. |
z |
terra::SpatRaster generated with |
a |
terra::SpatRaster generated with |
m |
logical terra::SpatRaster with one layer. A binary mask with
|
spacing |
numeric vector of length one. Angular spacing (in degrees) between directions to process. |
laxity |
numeric vector of length one. Positive multiplier applied to
MAD to set classification thresholds. Default is |
fov |
numeric vector. Field of view in degrees. If more than one value is provided, they are tried in order when a method fails. |
method |
character vector of length one. Built-in method to apply.
Available options are |
fun |
|
parallel |
logical vector of length one. If |
cores |
numeric vector of length one. Number of CPU cores to use when
|
logical |
logical vector of length one. Internally it is passed unchanged
to the argument |
leave_free |
numeric vector of length one. Number of CPU cores to leave
free when |
terra::SpatRaster object with two layers: "dn" for digital
number values and "n" for the number of valid pixels used in each
directional estimate.
This function is part of a manuscript currently under preparation.
There are no references for Rd macro \insertAllCites on this help page.
## Not run: caim <- read_caim() r <- caim$Blue z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) m <- !is.na(z) # Automatic sky brightness estimation sky <- apply_by_direction(r, z, a, m, spacing = 10, fov = c(30, 60), method = "detect_bg_dn", parallel = TRUE) plot(sky$dn) plot(r / sky$dn) # Using cone-shaped model sky_cs <- apply_by_direction(caim, z, a, m, spacing = 15, fov = 60, method = "fit_coneshaped_model", parallel = TRUE) plot(sky_cs$dn) # Using trend surface model sky_s <- apply_by_direction(caim, z, a, m, spacing = 15, fov = 60, method = "fit_trend_surface_np1", parallel = TRUE) plot(sky_s$dn) # Using a custom thresholding function thr <- apply_by_direction(r, z, a, m, 15, fov = c(30, 40, 50), fun = function(r, z, a, m) { thr <- tryCatch(thr_twocorner(r[m])$tm, error = function(e) NA) r[] <- thr r }, parallel = TRUE ) plot(thr$dn) plot(binarize_with_thr(r, thr$dn)) ## End(Not run)## Not run: caim <- read_caim() r <- caim$Blue z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) m <- !is.na(z) # Automatic sky brightness estimation sky <- apply_by_direction(r, z, a, m, spacing = 10, fov = c(30, 60), method = "detect_bg_dn", parallel = TRUE) plot(sky$dn) plot(r / sky$dn) # Using cone-shaped model sky_cs <- apply_by_direction(caim, z, a, m, spacing = 15, fov = 60, method = "fit_coneshaped_model", parallel = TRUE) plot(sky_cs$dn) # Using trend surface model sky_s <- apply_by_direction(caim, z, a, m, spacing = 15, fov = 60, method = "fit_trend_surface_np1", parallel = TRUE) plot(sky_s$dn) # Using a custom thresholding function thr <- apply_by_direction(r, z, a, m, 15, fov = c(30, 40, 50), fun = function(r, z, a, m) { thr <- tryCatch(thr_twocorner(r[m])$tm, error = function(e) NA) r[] <- thr r }, parallel = TRUE ) plot(thr$dn) plot(binarize_with_thr(r, thr$dn)) ## End(Not run)
Evaluate a user-supplied function locally using neighboring points defined on the hemispherical domain.
apply_locally( sampling_points, query_points = NULL, z, a, k = 20, angular_radius = 20, rule = "any", fun, parallel = TRUE, cores = NULL, logical = TRUE, leave_free = 1 )apply_locally( sampling_points, query_points = NULL, z, a, k = 20, angular_radius = 20, rule = "any", fun, parallel = TRUE, cores = NULL, logical = TRUE, leave_free = 1 )
sampling_points |
data.frame with columns |
query_points |
optional data.frame with columns |
z |
terra::SpatRaster generated with |
a |
terra::SpatRaster generated with |
k |
numeric vector of length one. Number of neighbors. |
angular_radius |
numeric vector of length one. The maximum radius for searching k-nearest neighbors (KNN) in degrees. |
rule |
character vector of length one. |
fun |
function. Must accept a data.frame (subset of |
parallel |
logical vector of length one. If |
cores |
numeric vector of length one. Number of CPU cores to use when
|
logical |
logical vector of length one. Internally it is passed unchanged
to the argument |
leave_free |
numeric vector of length one. Number of CPU cores to leave
free when |
The query_points argument lets the user evaluate fun at a custom set of
locations. For each point in query_points, neighbors are searched within
sampling_points. When query_points is NULL (default), the evaluation is
performed for every point in sampling_points. Neighbor selection is based on
spherical distance, see calc_spherical_distance(). The function fun is
then applied to the corresponding subset of neighboring sampling points.
The input passed to fun is a subset of rows of sampling_points, therefore,
fun must extract and process the column(s) of interest, e.g.: function(df) mean(df$dn).
The angular neighborhood is defined by two parameters:
k: number of nearest neighbors used by fun.
angular_radius: maximum allowed spherical distance (deg).
Argument rule controls how neighbor availability is interpreted:
"any": all neighbors found within angular_radius are passed to fun
(even if fewer than k).
"all": if fewer than k neighbors lie within the radius, the query
point is assigned NA.
A data.frame equal to query_points with one additional column, named
"out", containing the result of applying fun at each query point.
skygrid_centers() and fibonacci_points() for producing query_points.
## Not run: caim <- read_caim() r <- caim$Blue z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) # local thresholding sampling_points <- fibonacci_points(z, a, 3) display_caim(caim, sampling_points = sampling_points) sampling_points <- extract_dn(r, sampling_points, use_window = FALSE) head(sampling_points) eval_points <- apply_locally( sampling_points, fibonacci_points(z, a, 10), z, a, k = 500, angular_radius = 30, rule = "any", fun = function(df) thr_isodata(df$Blue) ) thr <- triangulate(eval_points[!is.na(eval_points$out),], r, col_id = 3) plot(thr) plot(binarize_with_thr(r, thr)) # Detect background DN sampling_points <- fibonacci_points(z, a, 2) sampling_points <- extract_dn(r, sampling_points, use_window = FALSE) query_points <- fibonacci_points(z, a, 10) eval_points <- apply_locally( sampling_points, query_points, z, a, k = 1000, angular_radius = 40, rule = "any", fun = function(df) { tryCatch(thr_twocorner(df[,3])$up, error = function(e) NA) }, parallel = TRUE ) i <- !is.na(eval_points$out) sky <- triangulate(eval_points[i, ], r, col_id = 3) plot(sky) plot(r/sky) ## End(Not run)## Not run: caim <- read_caim() r <- caim$Blue z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) # local thresholding sampling_points <- fibonacci_points(z, a, 3) display_caim(caim, sampling_points = sampling_points) sampling_points <- extract_dn(r, sampling_points, use_window = FALSE) head(sampling_points) eval_points <- apply_locally( sampling_points, fibonacci_points(z, a, 10), z, a, k = 500, angular_radius = 30, rule = "any", fun = function(df) thr_isodata(df$Blue) ) thr <- triangulate(eval_points[!is.na(eval_points$out),], r, col_id = 3) plot(thr) plot(binarize_with_thr(r, thr)) # Detect background DN sampling_points <- fibonacci_points(z, a, 2) sampling_points <- extract_dn(r, sampling_points, use_window = FALSE) query_points <- fibonacci_points(z, a, 10) eval_points <- apply_locally( sampling_points, query_points, z, a, k = 1000, angular_radius = 40, rule = "any", fun = function(df) { tryCatch(thr_twocorner(df[,3])$up, error = function(e) NA) }, parallel = TRUE ) i <- !is.na(eval_points$out) sky <- triangulate(eval_points[i, ], r, col_id = 3) plot(sky) plot(r/sky) ## End(Not run)
Quantify how a set of sky sampling points covers an equal-area hemispherical segmentation using (i) Kullback–Leibler divergence from a uniform distribution and (ii) coverage, defined as the fraction of cells receiving at least one point.
assess_sampling_uniformity(sky_points, equalarea_seg)assess_sampling_uniformity(sky_points, equalarea_seg)
sky_points |
|
equalarea_seg |
terra::SpatRaster generated with |
Points are projected onto an equal-area segmentation (equalarea_seg), and
cell-wise counts are computed.
Let be the total number of points,
the total number of cells, and the point count in cell
(). The empirical distribution for cell
is:
The uniform reference is
Kullback–Leibler divergence is computed excluding zero-probability terms:
Coverage is defined as:
List with two components:
klnumeric value. Kullback–Leibler divergence from uniformity.
coveragenumeric value in . Fraction of cells with at
least one point.
## Not run: caim <- read_caim() r <- caim$Blue z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) m <- !is.na(z) seg <- equalarea_segmentation(z, a, 20) bin <- binarize_by_region(r, seg, method = "thr_twocorner") bin <- bin & select_sky_region(z, 0, 80) g <- skygrid_segmentation(z, a, 5) sky_points <- sample_sky_points(r, bin, g, dist_to_black = 3) plot(r) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) seg <- equalarea_segmentation(z, a, 200) kl <- lapply(1:10, function(i) { sky_points <- rem_nearby_points(sky_points, NULL, z, a, i, space = "spherical") assess_sampling_uniformity(sky_points, seg)$kl }) plot(unlist(kl), type = "l") ## End(Not run)## Not run: caim <- read_caim() r <- caim$Blue z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) m <- !is.na(z) seg <- equalarea_segmentation(z, a, 20) bin <- binarize_by_region(r, seg, method = "thr_twocorner") bin <- bin & select_sky_region(z, 0, 80) g <- skygrid_segmentation(z, a, 5) sky_points <- sample_sky_points(r, bin, g, dist_to_black = 3) plot(r) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) seg <- equalarea_segmentation(z, a, 200) kl <- lapply(1:10, function(i) { sky_points <- rem_nearby_points(sky_points, NULL, z, a, i, space = "spherical") assess_sampling_uniformity(sky_points, seg)$kl }) plot(unlist(kl), type = "l") ## End(Not run)
Creates a single-layer raster in which pixel values represent azimuth angles, assuming an upwards-looking hemispherical photograph with the optical axis vertically aligned.
azimuth_image(z, orientation = 0)azimuth_image(z, orientation = 0)
z |
terra::SpatRaster generated with |
orientation |
numeric vector of length one. Azimuth angle (in degrees) corresponding to the direction at which the top of the image was pointing when the picture was taken. This design follows the common field protocol of recording the angle at which the top of the camera points. |
terra::SpatRaster with the same dimensions as the input
zenith image. Each pixel contains the azimuth angle in degrees, with zero
representing North and angles increasing counter-clockwise. The
object carries attributes orientation and lens_coef.
If orientation = 0, North (0 deg) is located at the top of the image, as in
conventional maps, but East (90 deg) and West (270 deg) appear flipped
relative to maps. To understand this, take two flash-card-sized pieces of
paper. Place one on a table in front of you and draw a compass rose on it.
Hold the other above your head, with the side facing down toward you, and
draw another compass rose following the directions from the one on the table.
This mimics the situation of taking an upwards-looking photo with a
smartphone while viewing the screen, and it will result in a mirrored
arrangement. Compare both drawings to see the inversion.
z <- zenith_image(600, lens("Nikon_FCE9")) a <- azimuth_image(z) plot(a) ## Not run: a <- azimuth_image(z, 45) plot(a) ## End(Not run)z <- zenith_image(600, lens("Nikon_FCE9")) a <- azimuth_image(z) plot(a) ## Not run: a <- azimuth_image(z, 45) plot(a) ## End(Not run)
Perform thresholding of greyscale images by applying a method regionally, using a segmentation map.
binarize_by_region(r, seg, method)binarize_by_region(r, seg, method)
r |
numeric terra::SpatRaster of one layer. Typically the blue channel of a canopy photograph. |
seg |
numeric terra::SpatRaster of one layer. A labeled
segmentation map defining the regions over which to apply the thresholding
method. Ring segmentation (see |
method |
character vector of length one. Name of the thresholding method to apply. See Details. |
This function supports several thresholding methods applied within the
regions defined by seg:
autothresholdr package:Any method supported by
autothresholdr::auto_thresh() can be used by specifying its name.
For example, "IsoData" applies the classic iterative intermeans algorithm
Ridler and Calvard (1978), which is among the most recommended
for canopy photography (Jonckheere et al. 2005).
Use "thr_isodata" to apply
thr_isodata(), a native implementation of the same algorithm
Use "thr_twocorner" to apply
thr_twocorner(), which implements a geometric thresholding strategy based
on identifying inflection points in the histogram, first introduced to
canopy photography by Macfarlane (2011). Since
this method might fail, the fallback is thr_isodata
Logical terra::SpatRaster (TRUE for sky, FALSE for
non-sky) of the same dimensions as r.
When methods from the autothresholdr package are used, r values
should be constrained to the range . See normalize_minmax().
Jonckheere I, Nackaerts K, Muys B, Coppin P (2005).
“Assessment of automatic gap fraction estimation of forests from digital hemispherical photography.”
Agricultural and Forest Meteorology, 132(1-2), 96–114.
doi:10.1016/j.agrformet.2005.06.003.
Leblanc SG, Chen JM, Fernandes R, Deering DW, Conley A (2005).
“Methodology comparison for canopy structure parameters extraction from digital hemispherical photography in boreal forests.”
Agricultural and Forest Meteorology, 129(3–4), 187–207.
ISSN 0168-1923.
doi:10.1016/j.agrformet.2004.09.006.
Macfarlane C (2011).
“Classification method of mixed pixels does not affect canopy metrics from digital images of forest overstorey.”
Agricultural and Forest Meteorology, 151(7), 833–840.
doi:10.1016/j.agrformet.2011.01.019.
Ridler TW, Calvard S (1978).
“Picture thresholding using an iterative selection method.”
IEEE Transactions on Systems, Man, and Cybernetics, 8(8), 630–632.
doi:10.1109/tsmc.1978.4310039.
## Not run: path <- system.file("external/DSCN4500.JPG", package = "rcaiman") zenith_colrow <- c(1276, 980) diameter <- 756*2 caim <- read_caim(path, zenith_colrow - diameter/2, diameter, diameter) z <- zenith_image(ncol(caim), lens("Nikon_FCE9")) r <- invert_gamma_correction(caim$Blue) r <- correct_vignetting(r, z, c(0.0638, -0.101)) %>% normalize_minmax() rings <- ring_segmentation(z, 15) bin <- binarize_by_region(r, rings, "thr_twocorner") plot(bin) ## End(Not run)## Not run: path <- system.file("external/DSCN4500.JPG", package = "rcaiman") zenith_colrow <- c(1276, 980) diameter <- 756*2 caim <- read_caim(path, zenith_colrow - diameter/2, diameter, diameter) z <- zenith_image(ncol(caim), lens("Nikon_FCE9")) r <- invert_gamma_correction(caim$Blue) r <- correct_vignetting(r, z, c(0.0638, -0.101)) %>% normalize_minmax() rings <- ring_segmentation(z, 15) bin <- binarize_by_region(r, rings, "thr_twocorner") plot(bin) ## End(Not run)
Apply a threshold or a raster of thresholds to a grayscale image, producing a binary image.
binarize_with_thr(r, thr)binarize_with_thr(r, thr)
r |
numeric terra::SpatRaster with one layer. |
thr |
either a numeric vector of length one (for global thresholding) or a numeric terra::SpatRaster with one layer (for local thresholding). |
This function supports both global and pixel-wise thresholding. It is a
wrapper around the > operator from the terra package. If a single numeric
threshold is provided via thr, it is applied globally to all pixels in r.
If instead a terra::SpatRaster object is provided, local thresholding
is performed, where each pixel is compared to its corresponding threshold
value.
This is useful after estimating thresholds using thr_twocorner(),
thr_isodata(), or
apply_by_direction(method = "thr_isodata"), among other posibilities.
Logical terra::SpatRaster (TRUE for sky, FALSE for
non-sky) with the same dimensions as r.
For global thresholding, thr must be greater than or equal to the minimum
value of r and lower than its maximum value.
r <- read_caim() bin <- binarize_with_thr(r$Blue, thr_isodata(r$Blue[])) plot(bin) ## Not run: # This function is also compatible with thresholds estimated using # the 'autothresholdr' package: require(autothresholdr) r <- r$Blue r <- normalize_minmax(r) %>% multiply_by(255) %>% round() thr <- auto_thresh(r[], "IsoData")[1] bin <- binarize_with_thr(r, thr) plot(bin) ## End(Not run)r <- read_caim() bin <- binarize_with_thr(r$Blue, thr_isodata(r$Blue[])) plot(bin) ## Not run: # This function is also compatible with thresholds estimated using # the 'autothresholdr' package: require(autothresholdr) r <- r$Blue r <- normalize_minmax(r) %>% multiply_by(255) %>% round() thr <- auto_thresh(r[], "IsoData")[1] bin <- binarize_with_thr(r, thr) plot(bin) ## End(Not run)
Calculate the diameter in pixels of a 180 deg fisheye image.
calc_diameter(lens_coef, radius, angle)calc_diameter(lens_coef, radius, angle)
lens_coef |
numeric vector. Polynomial coefficients of the lens
projection function. See |
radius |
numeric vector. Distance in pixels from the zenith. |
angle |
numeric vector. Zenith angle in degrees. |
This function is useful when the recording device has a field of view smaller
than 180 deg. Given a lens projection function and data points consisting of
radii (pixels) and their corresponding zenith angles (), it
returns the horizon radius (i.e., the radius for equal to 90 deg).
When working with non-circular hemispherical photography, this function
helps determine the diameter that a circular image would have if the
equipment recorded the whole hemisphere, required to build the
correct zenith image to use as input for expand_noncircular().
The required data (radius–angle pairs) can be obtained following the instructions in the user manual of Hemisfer software. A slightly simpler alternative is:
Find a vertical wall and a leveled floor, both well-constructed.
Draw a triangle of meters on the floor, with the
4-meter side along the wall.
Place the camera over the vertex 3 meters away from the wall, at a chosen height (e.g., 1.3 m).
Make a mark on the wall at the chosen height over the wall-vertex nearest
to the camera vertex. Make four more marks at 1 m intervals along a
horizontal line. This creates marks for 0, 18, 34, 45, and 54 deg
.
Before taking the photograph, align the zenith coordinates with the 0 deg
mark and ensure the optical axis is level.
The line selection tool of ImageJ can be used to measure the distance in pixels between points on the image. Draw a line and use the menu Analyze > Measure to obtain its length.
For obtaining the projection of a new lens, see calibrate_lens().
Numeric vector of length one. Estimated diameter in pixels, rounded
to the nearest even integer (see zenith_image() for details).
# Nikon D50 and Fisheye Nikkor 10.5mm lens calc_diameter(lens("Nikkor_10.5mm"), 1202, 54)# Nikon D50 and Fisheye Nikkor 10.5mm lens calc_diameter(lens("Nikkor_10.5mm"), 1202, 54)
Convert zenith angles (degrees) to normalized radial distance using the lens projection model.
calc_relative_radius(angle, lens_coef)calc_relative_radius(angle, lens_coef)
angle |
numeric vector. Zenith angles in degrees. |
lens_coef |
numeric vector. Polynomial coefficients of the lens
projection function. See |
This helper maps zenith angle(s) to a relative radius in [0, 1] given the lens projection coefficients.
Numeric vector of the same length as angle, constrained to [0, 1].
calc_relative_radius(45, lens())calc_relative_radius(45, lens())
Computes the angular distance, in radians, between directions defined by zenith and azimuth angles on the unit sphere.
calc_spherical_distance(z1, a1, z2, a2)calc_spherical_distance(z1, a1, z2, a2)
z1 |
numeric vector. Zenithal angle in radians. |
a1 |
numeric vector. Azimuthal angle in radians. |
z2 |
numeric vector of length one. Zenithal angle in radians. |
a2 |
numeric vector of length one. Azimuthal angle in radians. |
This function calculates the angle between two directions originating from the center of a unit sphere, using spherical trigonometry. The result is commonly referred to as spherical distance or angular distance. These terms are interchangeable when the sphere has radius one, as is standard in many applications, including celestial coordinate systems and, by extension, canopy hemispherical photography.
Spherical distance corresponds to the arc length of the shortest path between two points on the surface of a sphere. When the radius is one, this arc length equals the angle itself, expressed in radians.
Numeric vector of the same length as z1 and a1, containing the
spherical distance (in radians) from each (z1, a1) point to the
reference direction (z2, a2).
set.seed(1) z1 <- rnorm(10, 45, 20) * pi/180 a1 <- rnorm(10, 180, 90) * pi/180 calc_spherical_distance(z1, a1, 0, 0)set.seed(1) z1 <- rnorm(10, 45, 20) * pi/180 a1 <- rnorm(10, 180, 90) * pi/180 calc_spherical_distance(z1, a1, 0, 0)
Calculate zenith raster coordinates from points digitized with the open-source software package ‘ImageJ’.
calc_zenith_colrow(path_to_csv)calc_zenith_colrow(path_to_csv)
path_to_csv |
character vector of length one. Path to CSV file created with the ImageJ point selection tool. |
In this context, “zenith” denotes the location in the image that corresponds to the projection of the vertical direction when the optical axis is aligned vertically.
The technique described under the headline ‘Optical center characterization’ of the user manual of the software Can-Eye can be used to acquire the data for determining the zenith coordinates. This technique was used by Pekin and Macfarlane (2009), among others. Briefly, it consists in drilling a small hole in the cap of the fisheye lens (away from the center), and taking about ten photographs without removing the cap. The cap must be rotated about 30º before taking each photograph.
The point selection tool of ‘ImageJ’ software should be used to manually digitize the white dots and create a CSV file to feed this function. After digitizing the points on the image, use the dropdown menu Analyze>Measure to open the Results window. To obtain the CSV file, use File>Save As...
Another method (only valid when enough of the circle perimeter is depicted in the image) is taking a very bright picture (e.g., of a white-painted corner of a room) with the lens uncovered (do not use any mount). Then, digitize points over the circle perimeter. This was the method used for producing the example file (see Examples). It is worth noting that the perimeter of the circle depicted in a circular hemispherical photograph is not necessarily the horizon.
Numeric vector of length two. Raster coordinates of the zenith. These coordinates follow image (raster) convention: the origin is in the upper-left, and the vertical axis increases downward, like a spreadsheet. This contrasts with Cartesian coordinates, where the vertical axis increases upward.
This function assumes that all data points belong to the same circle, meaning that it does not support multiple holes when the Can-Eye procedure of drilling the lens cap is applied. The circle is fitted using the method presented by Kasa (1976).
Kasa I (1976).
“A circle fitting procedure and its error analysis.”
IEEE Transactions on Instrumentation and Measurement, IM–25(1), 8–14.
ISSN 1557-9662.
doi:10.1109/tim.1976.6312298.
Pekin B, Macfarlane C (2009).
“Measurement of crown cover and leaf area index using digital cover photography and its application to remote sensing.”
Remote Sensing, 1(4), 1298–1320.
doi:10.3390/rs1041298.
## Not run: path <- system.file("external/points_over_perimeter.csv", package = "rcaiman") calc_zenith_colrow(path) ## End(Not run)## Not run: path <- system.file("external/points_over_perimeter.csv", package = "rcaiman") calc_zenith_colrow(path) ## End(Not run)
Calibrate a fisheye lens to derive the mathematical relationship between image-space radial distances from the zenith and zenith angles in hemispherical space (assuming upward-looking hemispherical photography with the optical axis vertically aligned).
calibrate_lens(path_to_csv, degree = 3)calibrate_lens(path_to_csv, degree = 3)
path_to_csv |
character vector. Path(s) to CSV file(s) created with the ImageJ point selection tool. See Note. |
degree |
numeric vector of length one. Polynomial model degree. |
Fisheye lenses have a wide field of view and radial symmetry with respect to distortion. This property allows precise fitting of a polynomial model to relate pixel distances to zenith angles. The method implemented here, known as the "simple method", is described in detail by Díaz et al. (2024).
List with named elements:
dsData frame used to fit the model.
modellm object fitted to pixel distance vs. zenith angle.
horizon_radiusRadius at 90 deg.
lens_coefNumeric vector of polynomial model coefficients for predicting relative radius.
zenith_colrowRaster coordinates of the zenith push pin.
max_thetaMaximum zenith angle (deg).
max_theta_pxDistance in pixels between the zenith and the maximum zenith angle.
this package and ImageJ
camera and lens
tripod
standard yoga mat
table at least as wide as the yoga mat width
twenty two push pins of different colors
one print of this sheet (A1 size, almost like a research poster).
scissors
some patience
Cut the sheet by the dashed line. Place the yoga mat extended on top of the
table. Place the sheet on top of the yoga mat. Align the dashed line with the
yoga mat border closest to you. Place push pins on each cross. If you are
gentle, the yoga mat will allow you to do that without damaging the table. Of
course, other materials could be used to obtain the same result, such as
cardboard, foam, nails, etc.
Place the camera on the tripod. Align its optical axis with the table while looking for getting an image showing the overlapping of the three pairs of push pins, as instructed in the print. In order to take care of the line of pins at 90º relative to the optical axis, it may be of help to use the naked eye to align the entrance pupil of the lens with the pins. The alignment of the push pins only guarantees the position of the lens entrance pupil, the leveling should be cheeked with an instrument, and the alignment between the optical axis and the radius of the zenith push pin should be taken into account. In practice, the latter is achieved by aligning the camera body with the orthogonal frame made by the quarter circle.
Take a photo and transfer it to the computer, open it with ImageJ, and use
the point selection tool
to digitize the push pins, starting from the zenith push pin and not skipping
any shown push pin. End with an additional point where the image meets the
surrounding black (or the last pixel in case there is not blackness because
it is not a circular hemispherical image. There is no need to follow the line
formed by the push pins). Then, use the dropdown menu Analyze>Measure to open
the window Results. To obtain the CSV, use File>Save As...
To calibrate different directions, think of the fisheye image as an analog clock. To calibrate 3 o'clock, attach the camera to the tripod in landscape mode while leaving the quarter-circle at the lens's right side. To calibrate 9 o'clock, rotate the camera to put the quarter-circle at the lens's left side. To calibrate 12 and 6 o'clock, do the same but with the camera in portrait mode.
Díaz GM, Lang M, Kaha M (2024). “Simple calibration of fisheye lenses for hemipherical photography of the forest canopy.” Agricultural and Forest Meteorology, 352, 110020. ISSN 0168-1923. doi:10.1016/j.agrformet.2024.110020.
test_lens_coef(), crosscalibrate_lens(), extract_radiometry()
path <- system.file("external/Results_calibration.csv", package = "rcaiman") calibration <- calibrate_lens(path) coefficients(calibration$model) calibration$lens_coef %>% signif(3) calibration$horizon_radius ## Not run: test_lens_coef(calibration$lens_coef) #MacOS and Windows tend to differ here test_lens_coef(c(0.628, 0.0399, -0.0217)) ## End(Not run) .fp <- function(theta, lens_coef) { x <- lens_coef[1:5] x[is.na(x)] <- 0 for (i in 1:5) assign(letters[i], x[i]) a * theta + b * theta^2 + c * theta^3 + d * theta^4 + e * theta^5 } plot(calibration$ds) theta <- seq(0, pi/2, pi/180) lines(theta, .fp(theta, coefficients(calibration$model)))path <- system.file("external/Results_calibration.csv", package = "rcaiman") calibration <- calibrate_lens(path) coefficients(calibration$model) calibration$lens_coef %>% signif(3) calibration$horizon_radius ## Not run: test_lens_coef(calibration$lens_coef) #MacOS and Windows tend to differ here test_lens_coef(c(0.628, 0.0399, -0.0217)) ## End(Not run) .fp <- function(theta, lens_coef) { x <- lens_coef[1:5] x[is.na(x)] <- 0 for (i in 1:5) assign(letters[i], x[i]) a * theta + b * theta^2 + c * theta^3 + d * theta^4 + e * theta^5 } plot(calibration$ds) theta <- seq(0, pi/2, pi/180) lines(theta, .fp(theta, coefficients(calibration$model)))
Segment a raster into square regions of equal size arranged in a chessboard-like pattern.
chessboard(r, size)chessboard(r, size)
r |
|
size |
Numeric vector of length one. Size (in pixels) of each square segment. Must be a positive integer. |
This function divides the extent of r into
non-overlapping square segments of the given size, producing a segmentation
map where each segment has a unique integer label. It can be an alternative
to skygrid_segmentation() in special cases.
terra::SpatRaster with one layer and integer values, where each unique value corresponds to a square-segment ID.
caim <- read_caim() seg <- chessboard(caim, 20) plot(caim$Blue) plot(extract_feature(caim$Blue, seg))caim <- read_caim() seg <- chessboard(caim, 20) plot(caim$Blue) plot(extract_feature(caim$Blue, seg))
Generate an image of relative radiance or luminance based on the CIE General Sky model.
cie_image(z, a, sun_angles, sky_coef)cie_image(z, a, sun_angles, sky_coef)
z |
terra::SpatRaster generated with |
a |
terra::SpatRaster generated with |
sun_angles |
named numeric vector of length two, with components
|
sky_coef |
numeric vector of length five. Parameters of the CIE sky model. |
terra::SpatRaster with one layer whose pixel values
represent relative luminance or radiance across the sky hemisphere,
depending on whether the data used to obtain sky_coef was luminance
or radiance.
Coefficient sets and formulation are available in cie_table.
z <- zenith_image(50, lens()) a <- azimuth_image(z) sky_coef <- cie_table[4,1:5] %>% as.numeric() sun_angles <- c(z = 45, a = 0) plot(cie_image(z, a, sun_angles, sky_coef))z <- zenith_image(50, lens()) a <- azimuth_image(z) sky_coef <- cie_table[4,1:5] %>% as.numeric() sun_angles <- c(z = 45, a = 0) plot(cie_image(z, a, sun_angles, sky_coef))
The Commission Internationale de l’Éclairage (CIE; International Commission
on Illumination) standard (CIE 2004) defines 15
pre-calibrated sky luminance distributions, each described by a pair of
analytical functions, the gradation function
,
and the indicatrix function
.
Combined, they can predict the relative radiance in any sky
direction as:
, where is the zenith angle, is the azimuth angle,
and are the zenith and azimuth of the sun disk.
cie_tablecie_table
data.frame with 15 rows and 8 columns:
agradation function parameter.
bgradation function parameter.
cindicatrix function parameter.
dindicatrix function parameter.
eindicatrix function parameter.
indicatrix_groupfactor with six categories and numerical tags.
general_sky_typefactor with three categories: "Overcast",
"Clear", and "Partly cloudy".
descriptionuser-friendly description of the sky type.
Li et al. (2016)
CIE (2004).
“ISO 15469:2004(E) / CIE S 011/E:2003 - Spatial distribution of daylight — CIE standard general sky.”
https://www.iso.org/standard/38608.html.
International Standard.
Li DHW, Lou S, Lam JC, Wu RHT (2016).
“Determining solar irradiance on inclined planes from classified CIE (International Commission on Illumination) standard skies.”
Energy, 101, 462–470.
doi:10.1016/j.energy.2016.02.054.
Compute three color-opponent gradients to enhance the visual separation between sky and canopy in hemispherical photographs, particularly under diffuse light or complex cloud patterns.
complementary_gradients(caim)complementary_gradients(caim)
caim |
numeric terra::SpatRaster with three layers named
|
The method exploits chromatic differences between the red, green, and blue bands, following a simplified opponent-color logic. Each gradient is normalized by total brightness and modulated by a logistic contrast function to reduce the influence of underexposed regions:
"green_magenta" = · logistic(brightness)
"yellow_blue" = · logistic(brightness)
"red_cyan" = · logistic(brightness)
The logistic(brightness) term is computed as:
where is the 10th percentile of brightness values
(), and is their interquartile range.
This weighting suppresses gradients in poorly exposed regions to reduce spurious values caused by low signal-to-noise ratios.
Numeric terra::SpatRaster with three layers and the same
geometry as caim. The layers ("green_magenta", "yellow_blue",
"red_cyan") are chromatic gradients modulated by brightness.
This function is part of a paper under preparation.
There are no references for Rd macro \insertAllCites on this help page.
## Not run: caim <- read_caim() com <- complementary_gradients(caim) plot(com) ## End(Not run)## Not run: caim <- read_caim() com <- complementary_gradients(caim) plot(com) ## End(Not run)
Calculate canopy openness from a binarized hemispherical image with angular coordinates.
compute_canopy_openness(bin, z, a, m = NULL, angle_width = 10)compute_canopy_openness(bin, z, a, m = NULL, angle_width = 10)
bin |
logical terra::SpatRaster with one layer. A binarized
hemispherical image. See |
z |
terra::SpatRaster generated with |
a |
terra::SpatRaster generated with |
m |
logical terra::SpatRaster with one layer. A binary mask with
|
angle_width |
numeric vector of length one. Angle in deg that must
divide both 0–360 and 0–90 into an integer number of segments. Retrieve a
set of valid values by running
|
Canopy openness is computed following the equation from Gonsamo et al. (2011):
where is the gap fraction in cell ,
and are the lower and upper zenith
angles of the cell, is the number of cells in the corresponding
zenith ring, and is the total number of cells.
When a mask is provided via the m argument, the equation is adjusted to
compensate for the reduced area of the sky vault:
The denominator ensures that the resulting openness value remains scale-independent. Without this normalization, masking would lead to underestimation, as the numerator alone assumes full hemispherical coverage.
Numeric vector of length one, constrained to the range .
Gonsamo A, Walter JN, Pellikka P (2011). “CIMES: A package of programs for determining canopy geometry and solar radiation regimes through hemispherical photographs.” Computers and Electronics in Agriculture, 79(2), 207–215. doi:10.1016/j.compag.2011.10.001.
caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) m <- select_sky_region(z, 0, 70) bin <- binarize_with_thr(caim$Blue, thr_isodata(caim$Blue[m])) plot(bin) compute_canopy_openness(bin, z, a, m, 10)caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) m <- select_sky_region(z, 0, 70) bin <- binarize_with_thr(caim$Blue, thr_isodata(caim$Blue[m])) plot(bin) compute_canopy_openness(bin, z, a, m, 10)
Create an RGB image that resembles a photo taken with a conventional lens, using a small patch from the example hemispherical image.
conventional_lens_image()conventional_lens_image()
This is a fixed crop and reorientation of read_caim(). It does not perform
any re-projection. Intended for documentating functions.
The following code was used to define the region:
caim <- read_caim() r <- caim$Blue z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) m <- rast(z) m[] <- calc_spherical_distance( z[] * pi / 180, a[] * pi / 180, 1,# hinge-angle 90 * pi / 180 ) m <- !binarize_with_thr(m, 30 * pi / 180) m[is.na(z)] <- 0 m x11() plot(m * caim$Blue) za <- click(c(z, a)) za row_col <- row_col_from_zenith_azimuth(z, a, za[,1], za[,2]) plot(caim$Blue) points(row_col$col, nrow(caim) - row_col$row, col = 2, pch = 10) mn_y <- min(nrow(caim) -row_col$row) mx_y <- max(nrow(caim) -row_col$row) mn_x <- min(row_col$col) mx_x <- max(row_col$col) r <- terra::crop(caim$Blue, terra::ext(mn_x, mx_x, mn_y, mx_y)) plot(r)
Three-layer terra::SpatRaster with bands in RGB order.
conventional_lens_image()conventional_lens_image()
Apply a vignetting correction to an image using a polynomial model.
correct_vignetting( r, z, lens_coef_v, method = "simple", model_type = "polynomial" )correct_vignetting( r, z, lens_coef_v, method = "simple", model_type = "polynomial" )
r |
terra::SpatRaster of one or more layers (e.g., RGB channels or binary masks) in fisheye projection. |
z |
terra::SpatRaster generated with |
lens_coef_v |
numeric vector. Coefficients of the vignetting function. See Details. |
model_type |
character vector of lenght one. Only |
scheme |
character vector of length one. Calibration scheme defining
the modeling assumptions used by |
Vignetting is the gradual reduction of image brightness toward the periphery. This function applies a parametric correction tailored to a specific hemispherical camera system that varies with zenith angle at the pixel level.
The selected calibration scheme defines the modeling conventions used by
rcaiman to represent and correct vignetting effects. Different schemes
correspond to different assumptions and degrees of freedom in the underlying
polynomial model.
A constrained polynomial scheme in which the vignetting function is
normalized such that . The effective model is
, where
is the zenith angle (in radians) and are the
polynomial coefficients. Polynomial degrees up to 6 are supported.
This scheme follows the approach described in
Díaz et al. (2024). See extract_radiometry() for guidance
on estimating these coefficients.
A flexible polynomial scheme in which all model parameters, including the
constant term, are estimated from data. The effective model is
, where
is the zenith angle (in radians) and are free
parameters. Up to 7 parameters are supported.
This scheme assumes that the input data reliably capture the true shape of
the vignetting function, and therefore applies no structural constraints on
the model parameters. It is typically used with data acquired using a
photometric sphere, following the methodology described in
Lang et al. (2010).
terra::SpatRaster with the same content as r but with
pixel values adjusted to correct for vignetting, preserving all other
properties (layers, names, extent, and CRS).
Díaz GM, Lang M, Kaha M (2024).
“Simple calibration of fisheye lenses for hemipherical photography of the forest canopy.”
Agricultural and Forest Meteorology, 352, 110020.
ISSN 0168-1923.
doi:10.1016/j.agrformet.2024.110020.
Lang M, Kuusk A, Mõttus M, Rautiainen M, Nilson T (2010).
“Canopy gap fraction estimation from digital hemispherical images using sky radiance models and a linear conversion method.”
Agricultural and Forest Meteorology, 150(1), 20–29.
doi:10.1016/j.agrformet.2009.08.001.
## Not run: path <- system.file("external/APC_0836.jpg", package = "rcaiman") caim <- read_caim(path) z <- zenith_image(2132, lens("Olloclip")) a <- azimuth_image(z) zenith_colrow <- c(1063, 771) caim <- expand_noncircular(caim, z, zenith_colrow) m <- !is.na(caim$Red) & !is.na(z) caim[!m] <- 0 bin <- binarize_with_thr(caim$Blue, thr_isodata(caim$Blue[m])) display_caim(caim$Blue, bin) caim <- invert_gamma_correction(caim, 2.2) caim <- correct_vignetting(caim, z, c(-0.0546, -0.561, 0.22)) %>% normalize_minmax() ## End(Not run)## Not run: path <- system.file("external/APC_0836.jpg", package = "rcaiman") caim <- read_caim(path) z <- zenith_image(2132, lens("Olloclip")) a <- azimuth_image(z) zenith_colrow <- c(1063, 771) caim <- expand_noncircular(caim, z, zenith_colrow) m <- !is.na(caim$Red) & !is.na(z) caim[!m] <- 0 bin <- binarize_with_thr(caim$Blue, thr_isodata(caim$Blue[m])) display_caim(caim$Blue, bin) caim <- invert_gamma_correction(caim, 2.2) caim <- correct_vignetting(caim, z, c(-0.0546, -0.561, 0.22)) %>% normalize_minmax() ## End(Not run)
Apply vignetting correction using the aperture-dependent empirical model described by Lang et al. (2010).
correct_vignetting_legacy(r, z, aperture)correct_vignetting_legacy(r, z, aperture)
r |
terra::SpatRaster of one or more layers (e.g., RGB channels or binary masks) in fisheye projection. |
z |
terra::SpatRaster generated with |
aperture |
numeric vector of length one. Aperture value used during image acquisition. |
This function implements the aperture-dependent vignetting model reported in Eq. (5) of Lang et al. (2010). The relative pixel value with respect to the image center is given by:
where is the zenith angle in degrees (0–90 deg) and
is the aperture value used during image acquisition.
This function is provided to support workflows based on that historical
calibration. It does not use the polynomial schemes implemented in
correct_vignetting().
Numeric terra::SpatRaster with the same geometry as r,
with pixel values divided by to correct for vignetting.
Lang M, Kuusk A, Mõttus M, Rautiainen M, Nilson T (2010). “Canopy gap fraction estimation from digital hemispherical images using sky radiance models and a linear conversion method.” Agricultural and Forest Meteorology, 150(1), 20–29. doi:10.1016/j.agrformet.2009.08.001.
Extracts a rectangular region of interest (ROI) from a canopy image. This
function complements read_caim() and read_caim_raw().
crop_caim(r, upper_left = NULL, width = NULL, height = NULL)crop_caim(r, upper_left = NULL, width = NULL, height = NULL)
r |
|
upper_left |
numeric vector of length two. Pixel coordinates of the
upper-left corner of the ROI, in the format |
width, height
|
numeric vector of length one. Size (in pixels) of the rectangular ROI to read. |
terra::SpatRaster object containing the same layers and values
as r but restricted to the selected ROI, preserving all other properties.
To load a specific subregion from the image, use the arguments upper_left,
width, and height. These are expressed in raster coordinates, similar to
a spreadsheet layout: columns first, then rows. In other words, specify
coordinates as c(column, row), not c(row, column), which is typical
in data.frame objects.
While any image editor can be used to obtain these values, this function was tested with ImageJ, particularly the Fiji distribution. A recommended workflow:
Open the image in Fiji.
Draw a rectangular selection.
Go to Edit > Selection > Specify... to read upper_left, width, and height.
rcaiman uses terra without geographic semantics: rasters are kept with
unit resolution (cell size = 1) and a standardized extent
ext(0, ncol, 0, nrow) with CRS EPSG:7589 (set_rcaiman_geometry()).
caim <- read_caim() ncell(caim) caim <- crop_caim(caim, c(231,334), 15, 10) ncell(caim) path <- system.file("external/DSCN4500.JPG", package = "rcaiman") caim <- read_caim(path) zenith_colrow <- c(1286, 986) horizon_radius <- 742 upper_left <- zenith_colrow - horizon_radius height <- width <- horizon_radius * 2 plot(crop_caim(caim, upper_left, width, height)) display_caim(crop_caim(caim, upper_left, width, height))caim <- read_caim() ncell(caim) caim <- crop_caim(caim, c(231,334), 15, 10) ncell(caim) path <- system.file("external/DSCN4500.JPG", package = "rcaiman") caim <- read_caim(path) zenith_colrow <- c(1286, 986) horizon_radius <- 742 upper_left <- zenith_colrow - horizon_radius height <- width <- horizon_radius * 2 plot(crop_caim(caim, upper_left, width, height)) display_caim(crop_caim(caim, upper_left, width, height))
Given two photographs taken from the same point (matching entrance pupils and
aligned optical axes), with calibrated and uncalibrated cameras, derives a
polynomial projection for the uncalibrated device. Intended for cases where a
camera calibrated with a method of higher accuracy than calibrate_lens() is
available, or when there is a main camera to which all other devices should
be adjusted.
Points must be digitized in tandem with ImageJ and saved as CSV files.
See calibrate_lens() for background and general concepts.
crosscalibrate_lens( path_to_csv_uncal, path_to_csv_cal, zenith_colrow_uncal, zenith_colrow_cal, diameter_cal, lens_coef, degree = 3 )crosscalibrate_lens( path_to_csv_uncal, path_to_csv_cal, zenith_colrow_uncal, zenith_colrow_cal, diameter_cal, lens_coef, degree = 3 )
path_to_csv_uncal, path_to_csv_cal
|
character vectors of length one. Paths to CSV files created with ImageJ’s point selection tool (uncalibrated and calibrated images, respectively). |
zenith_colrow_uncal, zenith_colrow_cal
|
numeric vectors of length two.
Raster coordinates of the zenith for the uncalibrated and calibrated
images; see |
diameter_cal |
numeric vector of length one. Image diameter (pixels) of the calibrated camera. |
lens_coef |
numeric vector. Lens projection coefficients of the calibrated camera. |
degree |
numeric vector of length one. Polynomial degree for the uncalibrated model fit (default 3). |
Estimate a lens projection for an uncalibrated camera by referencing a calibrated camera photographed from the exact same location.
List with components:
dsdata.frame with zenith angle (theta, radians) and pixel radius
(px) from the uncalibrated camera.
modellm object: polynomial fit of px ~ theta.
horizon_radiusnumeric vector of length one. Pixel radius at 90 deg.
lens_coefnumeric vector. Distortion coefficients normalized by
horizon_radius.
calibrate_lens(), calc_zenith_colrow()
Converts fuzzy membership values into a binary classification using a regional approach that preserves aggregation consistency between the fuzzy and binary representations.
defuzzify(mem, seg)defuzzify(mem, seg)
mem |
numeric terra::SpatRaster of one layer. Degree of membership in a fuzzy classification. |
seg |
single-layer terra::SpatRaster. Segmentation map
of r, typically created with functions such as |
The conversion is applied within segments defined by seg,
ensuring that, in each segment, the aggregated Boolean result matches the
aggregated fuzzy value. This approach is well suited for converting subpixel
estimates, such as gap fraction, into binary outputs.
Logical terra::SpatRaster of the same dimensions as mem,
where each pixel value represents the binary version of mem after
applying the regional defuzzification procedure.
This method is also available in the HSP software package. See
hsp_compat().
## Not run: caim <- read_caim() r <- caim$Blue z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) path <- system.file("external/example.txt", package = "rcaiman") sky_cie <- read_sky_cie(gsub(".txt", "", path), z, a) sky_above <- ootb_sky_above(sky_cie$model$rr$sky_points, z, a, sky_cie) ratio <- r / sky_above$dn_raster ratio <- normalize_minmax(ratio, 0, 1, TRUE) plot(ratio) g <- skygrid_segmentation(z, a, 10) bin2 <- defuzzify(ratio, g) plot(bin2) # unsatisfactory results due to light conditions ## End(Not run)## Not run: caim <- read_caim() r <- caim$Blue z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) path <- system.file("external/example.txt", package = "rcaiman") sky_cie <- read_sky_cie(gsub(".txt", "", path), z, a) sky_above <- ootb_sky_above(sky_cie$model$rr$sky_points, z, a, sky_cie) ratio <- r / sky_above$dn_raster ratio <- normalize_minmax(ratio, 0, 1, TRUE) plot(ratio) g <- skygrid_segmentation(z, a, 10) bin2 <- defuzzify(ratio, g) plot(bin2) # unsatisfactory results due to light conditions ## End(Not run)
Wrapper for EBImage::display() that streamlines the visualization of
canopy images, optionally overlaying binary masks and segmentation borders.
It is intended for quick inspection of processed or intermediate results in
a graphical viewer.
display_caim( caim = NULL, bin = NULL, seg = NULL, sampling_points = NULL, sun_row_col = NULL, sun_disk_size = 9 )display_caim( caim = NULL, bin = NULL, seg = NULL, sampling_points = NULL, sun_row_col = NULL, sun_disk_size = 9 )
caim |
terra::SpatRaster. Typically the output of |
bin |
logical terra::SpatRaster with one layer. A binarized
hemispherical image. See |
seg |
Segmentation map typically created with functions such as
|
sampling_points |
|
sun_row_col |
numeric |
sun_disk_size |
numeric vector of length one. Sun disk size in pixels. |
Invisible NULL. Called for side effects (image viewer popup).
## Not run: caim <- read_caim() r <- caim$Blue z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) # See fit_cie_model() for details on below file path <- system.file("external/sky_points.csv", package = "rcaiman") sky_points <- read.csv(path) sky_points <- sky_points[c("Y", "X")] colnames(sky_points) <- c("row", "col") head(sky_points) plot(caim$Blue) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) x11() # plot(caim$Blue) # sun_angles <- click(c(z, a), 1) %>% as.numeric() sun_angles <- c(z = 49.5, a = 27.4) #taken with above lines then hardcoded sun_row_col <- row_col_from_zenith_azimuth(z, a, sun_angles["z"], sun_angles["a"]) bin <- binarize_with_thr(r$Blue, thr_isodata(r$Blue[])) seg <- equalarea_segmentation(z, a, 200) # color image display_caim(caim) # greyscale display_caim(caim$Blue) # to check binarization quality (press `h` with the pointer on the image to # learn useful hotkeys) display_caim(caim$Blue, bin) # to see the segments display_caim(seg = seg) # to see the marks display_caim(caim, sampling_points = sky_points, sun_row_col = sun_row_col) ## End(Not run)## Not run: caim <- read_caim() r <- caim$Blue z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) # See fit_cie_model() for details on below file path <- system.file("external/sky_points.csv", package = "rcaiman") sky_points <- read.csv(path) sky_points <- sky_points[c("Y", "X")] colnames(sky_points) <- c("row", "col") head(sky_points) plot(caim$Blue) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) x11() # plot(caim$Blue) # sun_angles <- click(c(z, a), 1) %>% as.numeric() sun_angles <- c(z = 49.5, a = 27.4) #taken with above lines then hardcoded sun_row_col <- row_col_from_zenith_azimuth(z, a, sun_angles["z"], sun_angles["a"]) bin <- binarize_with_thr(r$Blue, thr_isodata(r$Blue[])) seg <- equalarea_segmentation(z, a, 200) # color image display_caim(caim) # greyscale display_caim(caim$Blue) # to check binarization quality (press `h` with the pointer on the image to # learn useful hotkeys) display_caim(caim$Blue, bin) # to see the segments display_caim(seg = seg) # to see the marks display_caim(caim, sampling_points = sky_points, sun_row_col = sun_row_col) ## End(Not run)
Segment a hemispherical view into n_cells equal-area cells in
spherical space.
equalarea_segmentation(z, a, n_cells)equalarea_segmentation(z, a, n_cells)
z |
terra::SpatRaster single-layer raster with zenith angle (degrees). |
a |
terra::SpatRaster single-layer raster with azimuth angle (degrees). |
n_cells |
numeric scalar. Target number of cells. |
Segmentation begins by creating n_cells initial rings through uniform
sampling in cos(z) space. The core logic of the algorithm is grouping k
initial rings into a larger ring and then subdividing that ring azimuthally
into k cells. This procedure ensures equal area while allowing the final
segments to adopt compact shapes. In this way, the larger rings become the
final rings, of which there will be n_rings = round(sqrt(n_cells)).
The final ring containing the zenith has k = 1, the ring touching the
horizon has k = n_rings, and the intermediate k values must be
determined. To impose a compactness structure across zenith angles, the
method uses the equisolid-angle projection:
where is the radial distance, is the focal length, and
is the zenith angle. Based on this transformation, the
expression round(2 * sin(theta / 2) * n_rings * x) is used to compute k,
with x being a scalar (estimated with optim()) that adjusts the sum of
k values to exactly match n_cells.
Because the true central zenith angles of the final rings are not known in
advance (because they depend on k) the procedure is iterated.
It starts by assuming rings of equal angular width to obtain provisional
central zenith angles, computes k, updates the ring boundaries from the
resulting grouping, recomputes the central zenith angles, and repeats until
convergence.
Single-layer terra::SpatRaster with integer values from 1 to n_cells.
## Not run: z <- zenith_image(500, lens()) a <- azimuth_image(500, lens()) seg <- equalarea_segmentation(z, a, n_cells = 512) plot(seg) ## End(Not run)## Not run: z <- zenith_image(500, lens()) a <- azimuth_image(500, lens()) seg <- equalarea_segmentation(z, a, n_cells = 512) plot(seg) ## End(Not run)
Estimates the sun’s zenith and azimuth angles (deg) from a canopy hemispherical photograph, using either direct detection of the solar disk or indirect cues from the circumsolar region.
estimate_sun_angles( r, z, a, bin, seg, angular_radius_sun = 30, mode = "obscured" )estimate_sun_angles( r, z, a, bin, seg, angular_radius_sun = 30, mode = "obscured" )
r |
numeric terra::SpatRaster of one layer. Typically the blue band of a canopy image. |
z |
terra::SpatRaster generated with |
a |
terra::SpatRaster generated with |
bin |
logical terra::SpatRaster of one layer. Binary image where
|
seg |
Segmentation map of r, typically created with functions such as
|
angular_radius_sun |
numeric vector of length one. Maximum angular radius (in degrees) used to define the circumsolar region. |
mode |
character vector of length one. Estimation mode:
|
This function can operate under two alternative assumptions for estimating the sun position:
The solar disk is visible or partially obscured; the position is inferred from localized brightness peaks. It is also useful when a sunscreen is used, as in Kuusk and Paas (2007).
The solar disk is not visible; the position is inferred from radiometric and spatial cues aggregated over the circumsolar region.
When mode = "veiled", seg and angular_radius_sun are ignored.
Estimates refer to positions above the horizon; therefore, estimated angles
may require further manipulation if the photograph was acquired under
crepuscular light.
Named numeric vector of length two, with names z and a,
representing the sun’s zenith and azimuth angles (in degrees).
A scientific article presenting and validating this method is currently under preparation.
Kuusk A, Paas M (2007). “Radiometric correction of hemispherical images.” ISPRS Journal of Photogrammetry and Remote Sensing, 61(6), 405–413. ISSN 0924-2716. doi:10.1016/j.isprsjprs.2006.10.005.
## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) m <- !is.na(z) r <- caim$Blue bin <- binarize_by_region(r, ring_segmentation(z, 15), "thr_isodata") & select_sky_region(z, 0, 88) seg <- skygrid_segmentation(z, a, 10) sun_angles <- estimate_sun_angles(r, z, a, bin, seg, angular_radius_sun = 30) row_col <- row_col_from_zenith_azimuth(z, a, sun_angles["z"], sun_angles["a"]) plot(caim$Blue) points(row_col[1,2], nrow(caim) - row_col[1,1], col = "yellow", pch = 8, cex = 3) ## End(Not run)## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) m <- !is.na(z) r <- caim$Blue bin <- binarize_by_region(r, ring_segmentation(z, 15), "thr_isodata") & select_sky_region(z, 0, 88) seg <- skygrid_segmentation(z, a, 10) sun_angles <- estimate_sun_angles(r, z, a, bin, seg, angular_radius_sun = 30) row_col <- row_col_from_zenith_azimuth(z, a, sun_angles["z"], sun_angles["a"]) plot(caim$Blue) points(row_col[1,2], nrow(caim) - row_col[1,1], col = "yellow", pch = 8, cex = 3) ## End(Not run)
Add NA margins to a hemispherical photograph to align radiance at the zenith
with the image center. In this context, “zenith” denotes the location in the image
that corresponds to the projection of the vertical direction when the optical
axis is aligned vertically. Intended for non-circular images.
expand_noncircular(caim, z, zenith_colrow)expand_noncircular(caim, z, zenith_colrow)
caim |
terra::SpatRaster. Typically the output of |
z |
terra::SpatRaster generated with |
zenith_colrow |
numeric vector of length two. Raster coordinates of the
zenith (column, row). See |
terra::SpatRaster with the same layers and pixel values as caim,
but with NA margins added to center the zenith.
rcaiman uses terra without geographic semantics: rasters are kept with
unit resolution (cell size = 1) and a standardized extent
ext(0, ncol, 0, nrow) with CRS EPSG:7589.
## Not run: # Non-circular fisheye images from a smartphone with an auxiliary Lens # (also applicable to non-circular fisheye images from DSLR cameras) path <- system.file("external/APC_0836.jpg", package = "rcaiman") caim <- read_caim(path) z <- zenith_image(2132, lens("Olloclip")) a <- azimuth_image(z) zenith_colrow <- c(1063, 771) caim <- expand_noncircular(caim, z, zenith_colrow) plot(caim$Blue, col = seq(0, 1, 1/255) %>% grey()) m <- !is.na(caim$Red) & !is.na(z) plot(m, add = TRUE, alpha = 0.3, legend = FALSE) ## End(Not run)## Not run: # Non-circular fisheye images from a smartphone with an auxiliary Lens # (also applicable to non-circular fisheye images from DSLR cameras) path <- system.file("external/APC_0836.jpg", package = "rcaiman") caim <- read_caim(path) z <- zenith_image(2132, lens("Olloclip")) a <- azimuth_image(z) zenith_colrow <- c(1063, 771) caim <- expand_noncircular(caim, z, zenith_colrow) plot(caim$Blue, col = seq(0, 1, 1/255) %>% grey()) m <- !is.na(caim$Red) & !is.na(z) plot(m, add = TRUE, alpha = 0.3, legend = FALSE) ## End(Not run)
Compute a robust coefficient of variation (CV) at each sky point using a
neighbourhood in a raster image.
extract_cv(r, sky_points)extract_cv(r, sky_points)
r |
terra::SpatRaster. Image from which the points were sampled (or any raster with identical dimensions). |
sky_points |
|
For each sky point, the function extracts the radiance values in a
window centered on the corresponding pixel of r, and
computes a robust CV defined as:
where MAD is the median absolute deviation. This measure is suited for detecting local fluctuations caused by bright canopy elements or mixed pixels, while being less sensitive to outliers than a mean–sd based CV.
The extraction procedure mirrors the spatial logic used in
extract_dn(), but instead of averaging the neighbourhood, it applies a
robust dispersion measure (MAD/median) to quantify local radiance
variability around each point.
Numeric vector of length equal to nrow(sky_points), containing one
robust CV value per sky point.
extract_dn() for extracting digital numbers and tune_sky_sampling() for
an application of this metric.
## Not run: caim <- read_caim() r <- caim$Blue z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) # See fit_cie_model() for details on below file path <- system.file("external/sky_points.csv", package = "rcaiman") sky_points <- read.csv(path) sky_points <- sky_points[c("Y", "X")] colnames(sky_points) <- c("row", "col") head(sky_points) plot(caim$Blue) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) extract_cv(caim$Blue, sky_points) ## End(Not run)## Not run: caim <- read_caim() r <- caim$Blue z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) # See fit_cie_model() for details on below file path <- system.file("external/sky_points.csv", package = "rcaiman") sky_points <- read.csv(path) sky_points <- sky_points[c("Y", "X")] colnames(sky_points) <- c("row", "col") head(sky_points) plot(caim$Blue) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) extract_cv(caim$Blue, sky_points) ## End(Not run)
Obtain digital numbers from a raster at positions defined by points, with optional local averaging.
extract_dn(r, sampling_points, use_window = TRUE)extract_dn(r, sampling_points, use_window = TRUE)
r |
terra::SpatRaster. Image from which the points were sampled (or any raster with identical dimensions). |
sampling_points |
|
use_window |
logical vector of length one. If |
Wraps terra::extract() to support a window
centered on each target pixel (local mean). When it is disabled, only the
central pixel value is retrieved.
data.frame containing the original sampling_points plus one column per
layer in r (named after the layers).
For instructions on manually digitizing points, see the “Digitizing
sky points with ImageJ” and “Digitizing sky points with QGIS” sections in
fit_cie_model().
sample_sky_points() for automatically sampling sky points.
## Not run: caim <- read_caim() r <- caim$Blue z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) # See fit_cie_model() for details on below file path <- system.file("external/sky_points.csv", package = "rcaiman") sky_points <- read.csv(path) sky_points <- sky_points[c("Y", "X")] colnames(sky_points) <- c("row", "col") head(sky_points) plot(caim$Blue) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) sky_points <- extract_dn(caim, sky_points) head(sky_points) # To aggregate DNs across points (excluding 'row' and 'col'): apply(sky_points[, -(1:2)], 2, mean, na.rm = TRUE) ## End(Not run)## Not run: caim <- read_caim() r <- caim$Blue z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) # See fit_cie_model() for details on below file path <- system.file("external/sky_points.csv", package = "rcaiman") sky_points <- read.csv(path) sky_points <- sky_points[c("Y", "X")] colnames(sky_points) <- c("row", "col") head(sky_points) plot(caim$Blue) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) sky_points <- extract_dn(caim, sky_points) head(sky_points) # To aggregate DNs across points (excluding 'row' and 'col'): apply(sky_points[, -(1:2)], 2, mean, na.rm = TRUE) ## End(Not run)
Extract a numeric or logical summary from segmented raster regions using a user-defined reducer, returning one value per segment as a raster map or a named vector.
extract_feature(r, seg, fun = mean, return = "raster", ignore_label_0 = TRUE)extract_feature(r, seg, fun = mean, return = "raster", ignore_label_0 = TRUE)
r |
numeric terra::SpatRaster with one layer. |
seg |
single-layer terra::SpatRaster. Segmentation map
of r, typically created with functions such as |
fun |
function taking a numeric/logical vector and returning a single
numeric or logical value (default |
return |
character of length one. Either |
ignore_label_0 |
logical of length one. If |
Segments labeled 0 can be ignored via ignore_label_0 = TRUE. The
function in fun must return a single numeric or logical value for any input
vector (e.g., mean, median, or a custom reducer).
If return = "raster", a terra::SpatRaster where each pixel
holds its segment’s feature value. If return = "vector", a named numeric
(or logical) vector with one value per segment.
r <- read_caim() z <- zenith_image(ncol(r),lens()) a <- azimuth_image(z) g <- skygrid_segmentation(z, a, 10) print(extract_feature(r$Blue, g, return = "vector")) # plot(extract_feature(r$Blue, g, return = "raster"))r <- read_caim() z <- zenith_image(ncol(r),lens()) a <- azimuth_image(z) g <- skygrid_segmentation(z, a, 10) print(extract_feature(r$Blue, g, return = "vector")) # plot(extract_feature(r$Blue, g, return = "raster"))
Buid a datasets for vignetting modeling by sistematically extracting
radiometry from images taken with the aid of a portable light source and the
calibration board detailed in calibrate_lens().
extract_radiometry(l, size_px = NULL)extract_radiometry(l, size_px = NULL)
l |
list of preprocessed images (terra::SpatRaster) suitable for radiometry sampling. Images must comply with the equidistant projection. |
size_px |
numeric vector of length one. Diameter (pixels) of the
circular sampling area at the image center; off-center, the sampled region
becomes an ellipse under the equidistant projection. If |
data.frame with two columns:
thetazenith angle in radians.
radiometrynormalized digital number.
Lenses have the inconvenient property of increasingly attenuating light along the direction orthogonal to the optical axis. This phenomenon is known as the vignetting effect and varies with lens model and aperture setting. The method outlined here, known as the simple method, is explained in details in Díaz et al. (2024). Next explanation might serve mostly as a quick recap of it.
The development of the simple method was done with a Kindle Paperwhite eBooks
reader of 6" with built-in light. However, an iPhone 6 plus was also tested
in the early stages of development and no substantial differences were
observed. A metal bookends desk book holder was used to fasten the eBook
reader upright and a semi-transparent paper to favor a Lambertian light
distribution. In addition, the latter was used to draw on in order to
guide pixel sampling. The book holder also facilitated the alignment of the
screen with the dotted lines of the printed quarter-circle.
As a general guideline, a wide variety of mobile devices could be used as light sources, but if scattered data points are obtained with it, then other light sources should be tested in order to double check that the light quality is not the reason for points scattering.
With the room only illuminated by the portable light source, nine photographs
should be taken with the light source located in the equivalent to 0, 10, 20,
30, 40, 50, 60, 70, and 80 degrees of zenith angle, respectively. Camera
configuration should be in manual mode and set with the aperture (f/number)
for which a vignetting function is required. The shutter speed should be
regulated to obtain light-source pixels with middle grey values. The nine
photographs should be taken without changing the camera configuration and
the light conditions.
This code exemplifies how to use the function to obtain data and base R
functions to obtain the vignetting function ().
zenith_colrow <- c(1500, 997)
diameter <- 947*2
z <- zenith_image(diameter, c(0.689, 0.0131, -0.0295))
a <- azimuth_image(z)
.read_raw <- function(path_to_raw_file) {
r <- read_caim_raw(path_to_raw_file, only_blue = TRUE)
r <- crop_caim(r, zenith_colrow - diameter/2, diameter, diameter)
r <- fisheye_to_equidistant(r, z, a, m, radius = diameter/2,
k = 1, p = 1, rmax = 100)
}
l <- Map(.read_raw, dir("raw/up/", full.names = TRUE))
up_data <- extract_radiometry(l)
l <- Map(.read_raw, dir("raw/down/", full.names = TRUE))
down_data <- extract_radiometry(l)
l <- Map(.read_raw, dir("raw/right/", full.names = TRUE))
right_data <- extract_radiometry(l)
l <- Map(.read_raw, dir("raw/left/", full.names = TRUE))
left_data <- extract_radiometry(l)
ds <- rbind(up_data, down_data, right_data, left_data)
plot(ds, xlim = c(0, pi/2), ylim= c(0.5,1.05),
col = c(rep(1,9),rep(2,9),rep(3,9),rep(4,9)))
legend("bottomleft", legend = c("up", "down", "right", "left"),
col = 1:4, pch = 1)
fit <- lm((1 - ds$radiometry) ~ poly(ds$theta, 3, raw = TRUE) - 1)
summary(fit)
coef <- -fit$coefficients #did you notice the minus sign?
.fv <- function(x) 1 + coef[1] * x + coef[2] * x^2 + coef[3] * x^3
curve(.fv, add = TRUE, col = 2)
coef
Once one of the aperture settings is calibrated, it can be used to calibrate all the rest. To do so, the equipment should be used to take photographs in all desired exposition and without moving the camera, including the aperture already calibrated and preferably under an open sky in stable diffuse light conditions. Below code can be used as a template.
zenith_colrow <- c(1500, 997)
diameter <- 947*2
z <- zenith_image(diameter, c(0.689, 0.0131, -0.0295))
a <- azimuth_image(z)
files <- dir("raw/", full.names = TRUE)
l <- list()
for (i in seq_along(files)) {
if (i == 1) {
# because the first aperture was the one already calibrated
.read_raw <- function(path_to_raw_file) {
r <- read_caim_raw(path_to_raw_file, only_blue = TRUE)
r <- crop_caim(r, zenith_colrow - diameter/2, diameter, diameter)
r <- correct_vignetting(r, z, c(0.0302, -0.320, 0.0908))
r <- fisheye_to_equidistant(r, z, a, m, radius = diameter/2,
k = 1, p = 1, rmax = 100)
}
} else {
.read_raw <- function(path_to_raw_file) {
r <- read_caim_raw(path_to_raw_file, only_blue = TRUE)
r <- crop_caim(r, zenith_colrow - diameter/2, diameter, diameter)
r <- fisheye_to_equidistant(r, z, a, m, radius = diameter/2,
k = 1, p = 1, rmax = 100)
}
}
l[[i]] <- .read_raw(files[i])
}
ref <- l[[1]]
rings <- ring_segmentation(zenith_image(ncol(ref), lens()), 3)
theta <- seq(1.5, 90 - 1.5, 3) * pi/180
.fun <- function(r) {
r <- extract_feature(r, rings, return = "vector")
r/r[1]
}
l <- Map(.fun, l)
.fun <- function(x) {
x / l[[1]][] # because the first is the one already calibrated
}
radiometry <- Map(.fun, l)
l <- list()
for (i in 2:length(radiometry)) {
l[[i-1]] <- data.frame(theta = theta, radiometry = radiometry[[i]][])
}
ds <- l[[1]]
head(ds)
The result is one dataset (ds) for each file. This is all what it is needed before using base R functions to fit a vignetting function
This function does not fit the vignetting function itself. The output is a dataset to be used in subsequent modeling steps. See above sections for guidance.
Díaz GM, Lang M, Kaha M (2024). “Simple calibration of fisheye lenses for hemipherical photography of the forest canopy.” Agricultural and Forest Meteorology, 352, 110020. ISSN 0168-1923. doi:10.1016/j.agrformet.2024.110020.
Compute relative radiance at selected sky points by dividing their digital numbers (DN) by an estimated zenith DN.
extract_rr(r, z, a, sky_points, no_of_points = 3, use_window = TRUE)extract_rr(r, z, a, sky_points, no_of_points = 3, use_window = TRUE)
r |
terra::SpatRaster. Raster supplying the DN values; must
share rows and columns with the image used to obtain |
z |
terra::SpatRaster generated with |
a |
terra::SpatRaster generated with |
sky_points |
|
no_of_points |
numeric vector of length one or |
use_window |
logical vector of length one. If |
List with named elements:
zenith_dnnumeric. Estimated DN at the zenith.
sky_pointsdata.frame with columns row, col, a, z, dn,
and rr (pixel location, angular coordinates, extracted DN, and relative
radiance). If no_of_points is NULL, zenith_dn = 1 and dn = rr.
## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) # See fit_cie_model() for details on the CSV file path <- system.file("external/sky_points.csv", package = "rcaiman") sky_points <- read.csv(path) sky_points <- sky_points[c("Y", "X")] colnames(sky_points) <- c("row", "col") head(sky_points) plot(caim$Blue) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) rr <- extract_rr(caim$Blue, z, a, sky_points, 1) points(rr$sky_points$col, nrow(caim) - rr$sky_points$row, col = 3, pch = 0) ## End(Not run)## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) # See fit_cie_model() for details on the CSV file path <- system.file("external/sky_points.csv", package = "rcaiman") sky_points <- read.csv(path) sky_points <- sky_points[c("Y", "X")] colnames(sky_points) <- c("row", "col") head(sky_points) plot(caim$Blue) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) rr <- extract_rr(caim$Blue, z, a, sky_points, 1) points(rr$sky_points$col, nrow(caim) - rr$sky_points$row, col = 3, pch = 0) ## End(Not run)
Return image row and column indices for points distributed on the upper hemisphere using a spherical Fibonacci lattice with approximately constant angular spacing.
fibonacci_points(z, a, spacing)fibonacci_points(z, a, spacing)
z |
terra::SpatRaster generated with |
a |
terra::SpatRaster generated with |
spacing |
numeric vector of length one. Angular separation (deg) between |
data.frame with integer columns row and col.
z <- zenith_image(100, lens()) a <- azimuth_image(z) sampling_points <- fibonacci_points(z, a, 30) ## Not run: display_caim(is.na(z), sky_points = sky_points) ## End(Not run)z <- zenith_image(100, lens()) a <- azimuth_image(z) sampling_points <- fibonacci_points(z, a, 30) ## Not run: display_caim(is.na(z), sky_points = sky_points) ## End(Not run)
Reproject a hemispherical image from fisheye to equidistant projection (also known as polar projection) to standardize its geometry for subsequent analysis and comparison between images.
fisheye_to_equidistant(r, z, a, m, radius = NULL, k = 1, p = 1, rmax = 10)fisheye_to_equidistant(r, z, a, m, radius = NULL, k = 1, p = 1, rmax = 10)
r |
terra::SpatRaster of one or more layers (e.g., RGB channels or binary masks) in fisheye projection. |
z |
terra::SpatRaster generated with |
a |
terra::SpatRaster generated with |
m |
logical terra::SpatRaster with one layer. A binary mask with
|
radius |
numeric vector of length one. Radius (in pixels) of the
reprojected hemispherical image. Must be an integer value (no decimal
part). If |
k, p, rmax
|
numeric vector of length one. Parameters passed to
|
Pixel values and coordinates are treated as 3D points and reprojected
using Cartesian interpolation. Internally, this function uses
lidR::knnidw() as interpolation engine, so arguments k, p, and
rmax are passed to it without modification.
terra::SpatRaster with the same number of layers as r,
reprojected to equidistant projection with circular shape and radius
given by 'radius
## Not run: path <- system.file("external/APC_0836.jpg", package = "rcaiman") caim <- read_caim(path) calc_diameter(c(0.801, 0.178, -0.179), 1052, 86.2) z <- zenith_image(2216, c(0.801, 0.178, -0.179)) a <- azimuth_image(z) zenith_colrow <- c(1063, 771) caim <- expand_noncircular(caim, z, zenith_colrow) caim2 <- fisheye_to_equidistant(caim, z, a, !is.na(z), radius = 600) m <- !is.na(caim$Red) & select_sky_region(z, 0, 86.2) caim[!m] <- 0 m2 <- fisheye_to_equidistant(m, z, a, !is.na(z), radius = 600) m2 <- binarize_with_thr(m2, 0.5) #to turn it logical caim2[!m2] <- 0 plotRGB(normalize_minmax(caim2)*255) ## End(Not run)## Not run: path <- system.file("external/APC_0836.jpg", package = "rcaiman") caim <- read_caim(path) calc_diameter(c(0.801, 0.178, -0.179), 1052, 86.2) z <- zenith_image(2216, c(0.801, 0.178, -0.179)) a <- azimuth_image(z) zenith_colrow <- c(1063, 771) caim <- expand_noncircular(caim, z, zenith_colrow) caim2 <- fisheye_to_equidistant(caim, z, a, !is.na(z), radius = 600) m <- !is.na(caim$Red) & select_sky_region(z, 0, 86.2) caim[!m] <- 0 m2 <- fisheye_to_equidistant(m, z, a, !is.na(z), radius = 600) m2 <- binarize_with_thr(m2, 0.5) #to turn it logical caim2[!m2] <- 0 plotRGB(normalize_minmax(caim2)*255) ## End(Not run)
Reprojects a fisheye (hemispherical) image into a panoramic view using a cylindrical projection. The output is standardized so that rows correspond to zenith angle bands and columns to azimuthal sectors.
fisheye_to_pano(r, z, a, fun = mean, angle_width = 1)fisheye_to_pano(r, z, a, fun = mean, angle_width = 1)
r |
terra::SpatRaster of one or more layers (e.g., RGB channels or binary masks) in fisheye projection. |
z |
terra::SpatRaster generated with |
a |
terra::SpatRaster generated with |
fun |
function taking a numeric/logical vector and returning a single
numeric or logical value (default |
angle_width |
numeric vector of length one. Angle in deg that must
divide both 0–360 and 0–90 into an integer number of segments. Retrieve a
set of valid values by running
|
This function computes a cylindrical projection by aggregating pixel values
according to their zenith and azimuth angles. Internally, it creates a
segmentation grid with skygrid_segmentation() and applies
extract_feature() to compute a summary statistic (e.g., mean) of pixel
values within each cell.
terra::SpatRaster with rows representing zenith angle bands and
columns representing azimuthal sectors. The number of layers and names
matches that of the input r.
An early version of this function was used in Díaz et al. (2021).
Díaz GM, Negri PA, Lencinas JD (2021). “Toward making canopy hemispherical photography independent of illumination conditions: A deep-learning-based approach.” Agricultural and Forest Meteorology, 296, 108234. doi:10.1016/j.agrformet.2020.108234.
## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) pano <- fisheye_to_pano(caim, z, a) plotRGB(pano %>% normalize_minmax() %>% multiply_by(255)) ## End(Not run)## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) pano <- fisheye_to_pano(caim, z, a) plotRGB(pano %>% normalize_minmax() %>% multiply_by(255)) ## End(Not run)
Fit the CIE sky model to data sampled from a canopy photograph using general-purpose optimization.
fit_cie_model( rr, sun_angles, custom_sky_coef = NULL, std_sky_no = NULL, general_sky_type = NULL, optim_zenith_dn = FALSE, method = c("Nelder-Mead", "BFGS", "CG", "SANN") )fit_cie_model( rr, sun_angles, custom_sky_coef = NULL, std_sky_no = NULL, general_sky_type = NULL, optim_zenith_dn = FALSE, method = c("Nelder-Mead", "BFGS", "CG", "SANN") )
rr |
list, typically the output of
|
sun_angles |
named numeric vector of length two, with components
|
custom_sky_coef |
numeric vector of length five, or numeric matrix with five columns. Custom starting coefficients for optimization. If not provided, coefficients are initialized from standard skies. |
std_sky_no |
numeric vector. Standard sky numbers as in cie_table. If not provided, all are used. |
general_sky_type |
character vector of length one. Must be |
optim_zenith_dn |
logical vector of length one. Whether |
method |
character vector. Optimization methods passed to
|
The method is based on Lang et al. (2010).
For best results, the input data should show a linear relation between
digital numbers and the amount of light reaching the sensor. See
extract_radiometry() and read_caim_raw() for details.
As a compromise solution, invert_gamma_correction() can be used.
List with the following components:
rrThe input rr with an added pred column in
sky_points, containing predicted values.
opt_resultList returned by stats::optim().
coefNumeric vector of length five. CIE model coefficients.
sun_anglesNumeric vector of length two. Sun zenith and azimuth (degrees).
methodCharacter vector of length one. Optimization method used.
startNumeric vector of length five. Starting parameters.
metricNumeric value. Mean squared deviation as in Gauch et al. (2003).
The point selection tool of ‘ImageJ’ software can be used to manually digitize points and create a CSV file from which to read coordinates (see Examples). After digitizing the points on the image, this is a recommended workflow: 1. Use the dropdown menu Analyze > Measure to open the Results window. 2. Use File > Save As... to obtain the CSV file.
Use this code to create the input sky_points from ImageJ data:
sky_points <- read.csv(path)
sky_points <- sky_points[c("Y", "X")]
colnames(sky_points) <- c("row", "col")
To use the QGIS software to manually digitize points, drag and drop the image in an empty project, create an new vector layer, digitize points manually, save the editions, and close the project.
To create the new vector layer, this is a recommended workflow:
Fo to the dropdown menu Layer > Create Layer > New Geopackage Layer...
Choose "point" in the Geometry type dropdown list
Make sure the CRS is EPSG:7589.
Click on the Toogle Editing icon
Click on the Add Points Feature icon.
Use this code to create the input sky_points from QGIS data:
sky_points <- terra::vect(path)
sky_points <- terra::extract(caim, sky_points, cells = TRUE)
sky_points <- terra::rowColFromCell(caim, sky_points$cell) %>% as.data.frame()
colnames(sky_points) <- c("row", "col")
Gauch HG, Hwang JTG, Fick GW (2003).
“Model evaluation by comparison of model‐based predictions and measured values.”
Agronomy Journal, 95(6), 1442–1446.
ISSN 1435-0645.
doi:10.2134/agronj2003.1442.
Lang M, Kuusk A, Mõttus M, Rautiainen M, Nilson T (2010).
“Canopy gap fraction estimation from digital hemispherical images using sky radiance models and a linear conversion method.”
Agricultural and Forest Meteorology, 150(1), 20–29.
doi:10.1016/j.agrformet.2009.08.001.
## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) # Manual method following Lang et al. (2010) path <- system.file("external/sky_points.csv", package = "rcaiman") sky_points <- read.csv(path) sky_points <- sky_points[c("Y", "X")] colnames(sky_points) <- c("row", "col") head(sky_points) plot(caim$Blue) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) # x11() # plot(caim$Blue) # sun_angles <- click(c(z, a), 1) %>% as.numeric() sun_angles <- c(z = 49.5, a = 27.4) #taken with above lines then hardcoded sun_row_col <- row_col_from_zenith_azimuth(z, a, sun_angles["z"], sun_angles["a"]) points(sun_row_col[2], nrow(caim) - sun_row_col[1], col = "yellow", pch = 8, cex = 3) rr <- extract_rr(caim$Blue, z, a, sky_points) set.seed(7) model <- fit_cie_model(rr, sun_angles, general_sky_type = "Clear") plot(model$rr$sky_points$rr, model$rr$sky_points$pred) abline(0,1) lm(model$rr$sky_points$pred~model$rr$sky_points$rr) %>% summary() sky <- cie_image(z, a, model$sun_angles, model$coef) * model$rr$zenith_dn plot(sky) ratio <- caim$Blue/sky plot(ratio) plot(ratio > 1.05) plot(ratio > 1.15) ## End(Not run)## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) # Manual method following Lang et al. (2010) path <- system.file("external/sky_points.csv", package = "rcaiman") sky_points <- read.csv(path) sky_points <- sky_points[c("Y", "X")] colnames(sky_points) <- c("row", "col") head(sky_points) plot(caim$Blue) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) # x11() # plot(caim$Blue) # sun_angles <- click(c(z, a), 1) %>% as.numeric() sun_angles <- c(z = 49.5, a = 27.4) #taken with above lines then hardcoded sun_row_col <- row_col_from_zenith_azimuth(z, a, sun_angles["z"], sun_angles["a"]) points(sun_row_col[2], nrow(caim) - sun_row_col[1], col = "yellow", pch = 8, cex = 3) rr <- extract_rr(caim$Blue, z, a, sky_points) set.seed(7) model <- fit_cie_model(rr, sun_angles, general_sky_type = "Clear") plot(model$rr$sky_points$rr, model$rr$sky_points$pred) abline(0,1) lm(model$rr$sky_points$pred~model$rr$sky_points$rr) %>% summary() sky <- cie_image(z, a, model$sun_angles, model$coef) * model$rr$zenith_dn plot(sky) ratio <- caim$Blue/sky plot(ratio) plot(ratio > 1.05) plot(ratio > 1.15) ## End(Not run)
Fit a polynomial model to predict relative radiance from spherical coordinates using data sampled from a canopy photograph.
fit_coneshaped_model(sky_points, method = "zenith_n_azimuth")fit_coneshaped_model(sky_points, method = "zenith_n_azimuth")
sky_points |
|
method |
character. Model type to fit:
|
This model requires only sky_points, making it useful in workflows where
sun position cannot be reliably estimated, such as in apply_by_direction().
Otherwise, fit_cie_model() is a better choice.
Depending on method, it can fit:
See Díaz and Lencinas (2018) for details on the full model.
List with the following components:
funFunction taking zenith and azimuth (degrees) and returning
predicted relative radiance.
modellm object fitted by stats::lm().
Returns NULL (with a warning) if the number of input points is fewer than 20.
Díaz GM, Lencinas JD (2018). “Model-based local thresholding for canopy hemispherical photography.” Canadian Journal of Forest Research, 48(10), 1204–1216. doi:10.1139/cjfr-2018-0006.
## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) m <- !is.na(z) r <- caim$Blue bin <- binarize_by_region(r, ring_segmentation(z, 15), "thr_isodata") & select_sky_region(z, 0, 88) g <- skygrid_segmentation(z, a, 10, first_ring_different = TRUE) sky_points <- sample_sky_points(r, bin, g, dist_to_black = 3) plot(bin) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) rr <- extract_rr(r, z, a, sky_points) model <- fit_coneshaped_model(rr$sky_points) summary(model$model) sky_cs <- model$fun(z, a) * rr$zenith_dn plot(sky_cs) z_mini <- zenith_image(50, lens()) sky_cs <- model$fun(z_mini, azimuth_image(z_mini)) persp(sky_cs, theta = 90, phi = 20) ## End(Not run)## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) m <- !is.na(z) r <- caim$Blue bin <- binarize_by_region(r, ring_segmentation(z, 15), "thr_isodata") & select_sky_region(z, 0, 88) g <- skygrid_segmentation(z, a, 10, first_ring_different = TRUE) sky_points <- sample_sky_points(r, bin, g, dist_to_black = 3) plot(bin) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) rr <- extract_rr(r, z, a, sky_points) model <- fit_coneshaped_model(rr$sky_points) summary(model$model) sky_cs <- model$fun(z, a) * rr$zenith_dn plot(sky_cs) z_mini <- zenith_image(50, lens()) sky_cs <- model$fun(z_mini, azimuth_image(z_mini)) persp(sky_cs, theta = 90, phi = 20) ## End(Not run)
Fits a trend surface to sky digital numbers using spatial::surf.ls() as
the computational workhorse.
fit_trend_surface(sky_points, r, np = 6, col_id = "dn", extrapolate = FALSE)fit_trend_surface(sky_points, r, np = 6, col_id = "dn", extrapolate = FALSE)
sky_points |
|
r |
numeric terra::SpatRaster with one layer. Image from which
|
np |
degree of polynomial surface |
col_id |
numeric or character vector of length one. The name or position
of the column in |
extrapolate |
logical vector of length one. If |
This function models the variation in digital numbers across the sky dome
by fitting a polynomial surface in Cartesian space. It is intended to
capture smooth large-scale gradients and is more effective when called
via apply_by_direction().
List with named elements:
rasterterra::SpatRaster containing the fitted surface.
modelobject of class trls returned by spatial::surf.ls().
r2numeric value giving the coefficient of determination (R) of the fit.
There are no references for Rd macro \insertAllCites on this help page.
## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) m <- !is.na(z) r <- caim$Blue bin <- binarize_by_region(r, ring_segmentation(z, 15), "thr_isodata") & select_sky_region(z, 0, 88) g <- skygrid_segmentation(z, a, 10, first_ring_different = TRUE) sky_points <- sample_sky_points(r, bin, g, dist_to_black = 3) plot(bin) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) sky_points <- extract_dn(r, sky_points, use_window = TRUE) sky_s <- fit_trend_surface(sky_points, r, np = 4, col_id = 3, extrapolate = TRUE) plot(sky_s$raster) binarize_with_thr(r/sky_s$raster, 0.5) %>% plot() sky_s <- fit_trend_surface(sky_points, r, np = 6, col_id = 3, extrapolate = FALSE) plot(sky_s$raster) binarize_with_thr(r/sky_s$raster, 0.5) %>% plot() ## End(Not run)## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) m <- !is.na(z) r <- caim$Blue bin <- binarize_by_region(r, ring_segmentation(z, 15), "thr_isodata") & select_sky_region(z, 0, 88) g <- skygrid_segmentation(z, a, 10, first_ring_different = TRUE) sky_points <- sample_sky_points(r, bin, g, dist_to_black = 3) plot(bin) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) sky_points <- extract_dn(r, sky_points, use_window = TRUE) sky_s <- fit_trend_surface(sky_points, r, np = 4, col_id = 3, extrapolate = TRUE) plot(sky_s$raster) binarize_with_thr(r/sky_s$raster, 0.5) %>% plot() sky_s <- fit_trend_surface(sky_points, r, np = 6, col_id = 3, extrapolate = FALSE) plot(sky_s$raster) binarize_with_thr(r/sky_s$raster, 0.5) %>% plot() ## End(Not run)
Grow black pixels in a binary mask using a kernel of pixels
iteratively. Useful to reduce errors associated with inter-class borders.
grow_black(bin, dist_to_black)grow_black(bin, dist_to_black)
bin |
logical terra::SpatRaster with one layer. A binarized
hemispherical image. See |
dist_to_black |
numeric vector of length one. Buffer distance (pixels) used to expand black regions. |
Expands the regions with value FALSE (typically rendered as black) in a
binary image by applying a square-shaped buffer. Any white pixels (value
TRUE) within a distance equal to or less than dist_to_black from a black
pixel will be turned black.
Logical terra::SpatRaster with the same dimensions as bin. Compared
to the input bin, black regions (FALSE) have been expanded by the
specified buffer distance.
## Not run: r <- read_caim() bin <- binarize_with_thr(r$Blue, thr_isodata(r$Blue[])) plot(bin) bin <- grow_black(bin, 11) plot(bin) ## End(Not run)## Not run: r <- read_caim() bin <- binarize_with_thr(r$Blue, thr_isodata(r$Blue[])) plot(bin) bin <- grow_black(bin, 11) plot(bin) ## End(Not run)
Read and write legacy files from HSP (HemiSPherical Project Manager) projects
to interoperate with existing workflows. Intended for legacy support; not
required when working fully within rcaiman.
hsp_read_manual_input(path_to_HSP_project, img_name) hsp_read_opt_sky_coef(path_to_HSP_project, img_name) hsp_write_sky_points(sky_points, path_to_HSP_project, img_name) hsp_write_sun_coord(sun_row_col, path_to_HSP_project, img_name)hsp_read_manual_input(path_to_HSP_project, img_name) hsp_read_opt_sky_coef(path_to_HSP_project, img_name) hsp_write_sky_points(sky_points, path_to_HSP_project, img_name) hsp_write_sun_coord(sun_row_col, path_to_HSP_project, img_name)
path_to_HSP_project |
character vector of length one. Path to the HSP
project folder (e.g., |
img_name |
character vector of length one (e.g., |
sky_points |
|
sun_row_col |
numeric vector of length two. Raster coordinates (row, column) of the solar disk. |
See Functions
HSP was introduced in (Lang et al. 2013) and is based on the
method presented in Lang et al. (2010)). It runs
exclusively on Windows. HSP stores pre-processed images as PGM files in the
manipulate subfolder of each project (itself inside the projects folder).
hsp_read_manual_input()read sky marks and sun position
defined manually within an HSP project; returns a named list with
components weight, max_points, angle,
point_radius, sun_row_col, sky_points,
zenith_dn, masks, and marked_areas.
hsp_read_opt_sky_coef()read optimized CIE sky coefficients from an HSP project; returns a numeric vector of length five.
hsp_write_sky_points()write a file with sky point coordinates compatible with HSP; creates a file on disk.
hsp_write_sun_coord()write a file with solar disk coordinates compatible with HSP; creates a file on disk.
Lang M, Kodar A, Arumäe T (2013).
“Restoration of above canopy reference hemispherical image from below canopy measurements for plant area index estimation in forests.”
Forestry Studies, 59(1), 13–27.
doi:10.2478/fsmu-2013-0008.
Lang M, Kuusk A, Mõttus M, Rautiainen M, Nilson T (2010).
“Canopy gap fraction estimation from digital hemispherical images using sky radiance models and a linear conversion method.”
Agricultural and Forest Meteorology, 150(1), 20–29.
doi:10.1016/j.agrformet.2009.08.001.
## Not run: # NOTE: assumes the working directory is the HSP project folder (e.g., an RStudio project). # From HSP to R in order to compare --------------------------------------- r <- read_caim("manipulate/IMG_1013.pgm") z <- zenith_image(ncol(r), lens()) a <- azimuth_image(z) manual_input <- read_manual_input(".", "IMG_1013") sun_row_col <- manual_input$sun_row_col sun_angles <- zenith_azimuth_from_row_col( z, a, sun_row_col[1], sun_row_col[2] ) sun_angles <- as.vector(sun_angles) sky_points <- manual_input$sky_points rr <- extract_rr(r, z, a, sky_points) model <- fit_cie_model(rr, sun_angles) sky <- cie_image( z, a, model$sun_angles, model$coef ) * model$rr$zenith_dn plot(r / sky) r <- read_caim("manipulate/IMG_1013.pgm") sky_coef <- read_opt_sky_coef(".", "IMG_1013") sky_m <- cie_image(z, a, sun_angles, sky_coef) sky_m <- cie_sky_manual * manual_input$zenith_dn plot(r / sky_m) # From R to HSP ---------------------------------------------------------- r <- read_caim("manipulate/IMG_1014.pgm") z <- zenith_image(ncol(r), lens()) a <- azimuth_image(z) m <- !is.na(z) g <- skygrid_segmentation(z, a, 10) bin <- binarize_with_thr(caim$Blue, thr_isodata(caim$Blue[m])) bin <- select_sky_region(z, 0, 85) & bin sun_angles <- estimate_sun_angles(r, z, a, bin, g) sun_row_col <- row_col_from_zenith_azimuth( z, a, sun_angles["z"], sun_angles["a"] ) %>% as.numeric() write_sun_coord(sun_row_col, ".", "IMG_1014") sky_points <- sample_sky_points(r, bin, g) write_sky_points(sky_points, ".", "IMG_1014") ## End(Not run)## Not run: # NOTE: assumes the working directory is the HSP project folder (e.g., an RStudio project). # From HSP to R in order to compare --------------------------------------- r <- read_caim("manipulate/IMG_1013.pgm") z <- zenith_image(ncol(r), lens()) a <- azimuth_image(z) manual_input <- read_manual_input(".", "IMG_1013") sun_row_col <- manual_input$sun_row_col sun_angles <- zenith_azimuth_from_row_col( z, a, sun_row_col[1], sun_row_col[2] ) sun_angles <- as.vector(sun_angles) sky_points <- manual_input$sky_points rr <- extract_rr(r, z, a, sky_points) model <- fit_cie_model(rr, sun_angles) sky <- cie_image( z, a, model$sun_angles, model$coef ) * model$rr$zenith_dn plot(r / sky) r <- read_caim("manipulate/IMG_1013.pgm") sky_coef <- read_opt_sky_coef(".", "IMG_1013") sky_m <- cie_image(z, a, sun_angles, sky_coef) sky_m <- cie_sky_manual * manual_input$zenith_dn plot(r / sky_m) # From R to HSP ---------------------------------------------------------- r <- read_caim("manipulate/IMG_1014.pgm") z <- zenith_image(ncol(r), lens()) a <- azimuth_image(z) m <- !is.na(z) g <- skygrid_segmentation(z, a, 10) bin <- binarize_with_thr(caim$Blue, thr_isodata(caim$Blue[m])) bin <- select_sky_region(z, 0, 85) & bin sun_angles <- estimate_sun_angles(r, z, a, bin, g) sun_row_col <- row_col_from_zenith_azimuth( z, a, sun_angles["z"], sun_angles["a"] ) %>% as.numeric() write_sun_coord(sun_row_col, ".", "IMG_1014") sky_points <- sample_sky_points(r, bin, g) write_sky_points(sky_points, ".", "IMG_1014") ## End(Not run)
Interpolate values from canopy photographs using inverse distance weighting (IDW) with k-nearest neighbors in image (planar) coordinates. A radius limits neighbor search.
interpolate_planar( sky_points, r, k = 3, p = 2, rmax = 200, col_id = "dn", rigorous = FALSE, engine = "lidR" )interpolate_planar( sky_points, r, k = 3, p = 2, rmax = 200, col_id = "dn", rigorous = FALSE, engine = "lidR" )
sky_points |
|
r |
numeric terra::SpatRaster with one layer. Image from which
|
k, p, rmax
|
numeric vector of length one. Parameters passed to
|
col_id |
numeric or character vector of length one. The name or position
of the column in |
rigorous |
logical vector of length one. By default ( |
engine |
character vector of length one. Interpolation engine to be used.
Allowed choises are "lidR", in which case delegates to |
Defaults follow Lang et al. (2013). Note that rmax is
given in pixels but intended to approximate 15–20 deg in angular terms.
Therefore, this value needs fine-tuning based on image resolution and lens
projection. For best results, the interpolated quantity should be linearly
related to scene radiance; see extract_radiometry() and read_caim_raw().
For JPEG images, consider invert_gamma_correction() to reverse gamma
encoding.
Numeric terra::SpatRaster with one layer and the same geometry
as r.
No consistency checks are performed to ensure that sky_points and r
are geometrically compatible. Incorrect combinations may lead to invalid
outputs.
Lang M, Kodar A, Arumäe T (2013). “Restoration of above canopy reference hemispherical image from below canopy measurements for plant area index estimation in forests.” Forestry Studies, 59(1), 13–27. doi:10.2478/fsmu-2013-0008.
## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) m <- !is.na(z) r <- caim$Blue bin <- binarize_by_region(r, ring_segmentation(z, 15), "thr_isodata") & select_sky_region(z, 0, 88) g <- skygrid_segmentation(z, a, 10) sky_points <- sample_sky_points(r, bin, g, dist_to_black = 3) plot(bin) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) sky_points <- extract_dn(r, sky_points) sky <- interpolate_planar(sky_points, r, col_id = 3) plot(sky) plot(r/sky) ## End(Not run)## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) m <- !is.na(z) r <- caim$Blue bin <- binarize_by_region(r, ring_segmentation(z, 15), "thr_isodata") & select_sky_region(z, 0, 88) g <- skygrid_segmentation(z, a, 10) sky_points <- sample_sky_points(r, bin, g, dist_to_black = 3) plot(bin) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) sky_points <- extract_dn(r, sky_points) sky <- interpolate_planar(sky_points, r, col_id = 3) plot(sky) plot(r/sky) ## End(Not run)
Interpolate values from canopy photographs using inverse distance weighting (IDW) with k-nearest neighbors, computing distances in spherical coordinates that map the sky vault. Optionally blend with a model surface to fill voids.
interpolate_spherical( sky_points, z, a, filling_source = NULL, w = 1, k = 3, p = 2, angular_radius = 20, rule = "any", size = 50 )interpolate_spherical( sky_points, z, a, filling_source = NULL, w = 1, k = 3, p = 2, angular_radius = 20, rule = "any", size = 50 )
sky_points |
|
z |
terra::SpatRaster generated with |
a |
terra::SpatRaster generated with |
filling_source |
optional numeric terra::SpatRaster with one
layer. Surface used to complement |
w |
numeric vector of length one or single-layer
terra::SpatRaster. Weight assigned to |
k |
numeric vector of length one. Number of neighbors. |
p |
numeric vector of length one. Inverse distance weighting exponent. |
angular_radius |
numeric vector of length one. The maximum radius for searching k-nearest neighbors (KNN) in degrees. |
rule |
character vector of length one. Either |
size |
numeric vector of length one. Number of rows and columns of the low-resolution grid used before resampling to full resolution. |
Distances are great-circle distances on the sky vault (see
calc_spherical_distance()). When filling_source is provided, local IDW
estimates are blended with that surface following Eq. 6 in
Lang et al. (2010). For efficiency, interpolation runs on
a temporary low-resolution grid of size size.
Numeric terra::SpatRaster with one layer of interpolated
values and the geometry of z.
This function assumes that sky_points and the
terra::SpatRaster inputs are spatially aligned and share the same
geometry. No checks are performed to enforce this.
Lang M, Kuusk A, Mõttus M, Rautiainen M, Nilson T (2010). “Canopy gap fraction estimation from digital hemispherical images using sky radiance models and a linear conversion method.” Agricultural and Forest Meteorology, 150(1), 20–29. doi:10.1016/j.agrformet.2009.08.001.
## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) # Manual method following Lang et al. (2010) path <- system.file("external/sky_points.csv", package = "rcaiman") sky_points <- read.csv(path) sky_points <- sky_points[c("Y", "X")] colnames(sky_points) <- c("row", "col") head(sky_points) plot(caim$Blue) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) # x11() # plot(caim$Blue) # sun_angles <- click(c(z, a), 1) %>% as.numeric() sun_angles <- c(z = 49.5, a = 27.4) #taken with above lines then hardcoded sun_row_col <- row_col_from_zenith_azimuth(z, a, sun_angles["z"], sun_angles["a"]) points(sun_row_col[2], nrow(caim) - sun_row_col[1], col = "yellow", pch = 8, cex = 3) rr <- extract_rr(caim$Blue, z, a, sky_points) set.seed(7) model <- fit_cie_model(rr, sun_angles, general_sky_type = "Clear") sky_cie <- cie_image(z, a, model$sun_angles, model$coef) sky_rr <- interpolate_spherical(rr$sky_points, z, a, filling_source = sky_cie, w = 1, k = 10, p = 2, angular_radius = 20, rule = "any", size = 50) plot(caim$Blue/sky_rr/rr$zenith_dn) ## End(Not run)## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) # Manual method following Lang et al. (2010) path <- system.file("external/sky_points.csv", package = "rcaiman") sky_points <- read.csv(path) sky_points <- sky_points[c("Y", "X")] colnames(sky_points) <- c("row", "col") head(sky_points) plot(caim$Blue) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) # x11() # plot(caim$Blue) # sun_angles <- click(c(z, a), 1) %>% as.numeric() sun_angles <- c(z = 49.5, a = 27.4) #taken with above lines then hardcoded sun_row_col <- row_col_from_zenith_azimuth(z, a, sun_angles["z"], sun_angles["a"]) points(sun_row_col[2], nrow(caim) - sun_row_col[1], col = "yellow", pch = 8, cex = 3) rr <- extract_rr(caim$Blue, z, a, sky_points) set.seed(7) model <- fit_cie_model(rr, sun_angles, general_sky_type = "Clear") sky_cie <- cie_image(z, a, model$sun_angles, model$coef) sky_rr <- interpolate_spherical(rr$sky_points, z, a, filling_source = sky_cie, w = 1, k = 10, p = 2, angular_radius = 20, rule = "any", size = 50) plot(caim$Blue/sky_rr/rr$zenith_dn) ## End(Not run)
Approximates the inversion of the gamma encoding applied to JPEG images.
invert_gamma_correction(dn, gamma = 2.2)invert_gamma_correction(dn, gamma = 2.2)
dn |
numeric vector or terra::SpatRaster. Digital numbers from a JPEG file (range 0–255, as per standard 8-bit encoding). |
gamma |
numeric vector of length one. Exponent applied in the inverse gamma correction (typically 2.2 for sRGB). |
Digital cameras typically encode images using the sRGB color space, which applies a non-linear transformation—commonly referred to as gamma correction—to the sensor's linear luminance response. This function applies a power transformation to approximate the inverse of that encoding, restoring a response closer to linear.
Same properties as dn, with values adjusted by
inverse gamma correction and rescaled to the range .
There are no references for Rd macro \insertAllCites on this help page.
## Not run: path <- system.file("external/APC_0836.jpg", package = "rcaiman") caim <- read_caim(path) z <- zenith_image(2132, lens("Olloclip")) a <- azimuth_image(z) zenith_colrow <- c(1063, 771) caim <- expand_noncircular(caim, z, zenith_colrow) m <- !is.na(caim$Red) & !is.na(z) caim[!m] <- 0 bin <- binarize_with_thr(caim$Blue, thr_isodata(caim$Blue[m])) display_caim(caim$Blue, bin) caim <- invert_gamma_correction(caim, 2.2) ## End(Not run)## Not run: path <- system.file("external/APC_0836.jpg", package = "rcaiman") caim <- read_caim(path) z <- zenith_image(2132, lens("Olloclip")) a <- azimuth_image(z) zenith_colrow <- c(1063, 771) caim <- expand_noncircular(caim, z, zenith_colrow) m <- !is.na(caim$Red) & !is.na(z) caim[!m] <- 0 bin <- binarize_with_thr(caim$Blue, thr_isodata(caim$Blue[m])) display_caim(caim$Blue, bin) caim <- invert_gamma_correction(caim, 2.2) ## End(Not run)
Identify outlying values in a numeric vector using a robust MAD–median rule and return a logical mask indicating which elements are outliers.
is_outlier(x, y = NULL, laxity = 2, cutoff_side = "both")is_outlier(x, y = NULL, laxity = 2, cutoff_side = "both")
x |
numeric vector. Values to evaluate. |
y |
optinal numeric vector. Values from which median and MAD are
calculated. If |
laxity |
numeric vector of length one. Positive multiplier applied to
MAD to set classification thresholds. Default is |
cutoff_side |
character vector of length one. One of |
Apply the robust criterion from Leys et al. (2013) to
detect statistical outliers. The function returns a logical vector of the
same length as x with TRUE for detected outliers.
An observation is considered outlying if it satisfies:
where is the median of x and is its median absolute
deviation. The laxity parameter multiplies MAD to set the classification
threshold.
The cutoff_side argument controls which tail(s) are evaluated:
"both" (default) applies a two-sided test, "left" applies the left-tail
test only, and "right" applies the right-tail test only.
If the MAD is zero (no dispersion), all values are flagged as non-outliers. NA
values in x are treated as non-outliers.
Logical vector of the same length as x. TRUE indicates the outliers.
Leys C, Ley C, Klein O, Bernard P, Licata L (2013). “Detecting outliers: Do not use standard deviation around the mean, use absolute deviation around the median.” Journal of Experimental Social Psychology, 49(4), 764–766. ISSN 0022-1031. doi:10.1016/j.jesp.2013.03.013.
x <- c(rnorm(100), 10, 12) i <- is_outlier(x, x, laxity = 2.5, cutoff_side = "both") x[i] # values flagged as outliers x[!i] # filter outliersx <- c(rnorm(100), 10, 12) i <- is_outlier(x, x, laxity = 2.5, cutoff_side = "both") x[i] # values flagged as outliers x[!i] # filter outliers
Retrieve projection coefficients and field-of-view (FOV, deg) for known lenses. Coefficients expect zenith angle in radians and return relative radius.
lens(type = "equidistant", return = "coef")lens(type = "equidistant", return = "coef")
type |
Character vector of length one. Lens identifier. See Details. |
return |
Character vector of length one. Either |
In upward-looking leveled hemispherical photography, the zenith corresponds to the center of a circular image whose perimeter represents the horizon, assuming a lens with a 180° field of view. The relative radius is the radial distance to a given point, expressed as a fraction of the maximum radius (i.e., the horizon). The equidistant projection model, also called polar projection, is the standard reference model, characterized by a linear relationship between zenith angle and relative radius.
Real lenses deviate from ideal projections. Following Hemisfer software, this package models deviations with polynomial functions. All angular values are in radians.
Currently available lenses:
Ideal equidistant projection (Schneider et al. 2009).
AF DX Fisheye Nikkor 10.5mm f/2.8G ED (Pekin and Macfarlane 2009).
Nikon FC-E9 converter (Díaz et al. 2024).
Auxiliary lens for mobile devices manufactured by Olloclip (Díaz et al. 2024).
AF–S Fisheye Nikkor 8–15mm f/3.5–4.5E ED (Díaz et al. 2024).
See calibrate_lens() for fitting new projection functions.
Numeric vector. Depends on return:
Polynomial coefficients of the projection function
(relative radius as a function of theta, radians).
numeric vector of length one. Maximum field of view (deg).
Díaz GM, Lang M, Kaha M (2024).
“Simple calibration of fisheye lenses for hemipherical photography of the forest canopy.”
Agricultural and Forest Meteorology, 352, 110020.
ISSN 0168-1923.
doi:10.1016/j.agrformet.2024.110020.
Pekin B, Macfarlane C (2009).
“Measurement of crown cover and leaf area index using digital cover photography and its application to remote sensing.”
Remote Sensing, 1(4), 1298–1320.
doi:10.3390/rs1041298.
Schneider D, Schwalbe E, Maas H (2009).
“Validation of geometric models for fisheye lenses.”
ISPRS Journal of Photogrammetry and Remote Sensing, 64(3), 259–266.
doi:10.1016/j.isprsjprs.2009.01.001.
lens("Nikon_FCE9") lens("Nikon_FCE9", return = "fov") .fp <- function(theta, lens_coef) { x <- lens_coef[1:5] x[is.na(x)] <- 0 for (i in 1:5) assign(letters[i], x[i]) a * theta + b * theta^2 + c * theta^3 + d * theta^4 + e * theta^5 } theta <- seq(0, pi/2, pi/180) plot(theta, .fp(theta, lens()), type = "l", lty = 2, ylab = "relative radius") lines(theta, .fp(theta, lens("Nikon_FCE9")))lens("Nikon_FCE9") lens("Nikon_FCE9", return = "fov") .fp <- function(theta, lens_coef) { x <- lens_coef[1:5] x[is.na(x)] <- 0 for (i in 1:5) assign(letters[i], x[i]) a * theta + b * theta^2 + c * theta^3 + d * theta^4 + e * theta^5 } theta <- seq(0, pi/2, pi/180) plot(theta, .fp(theta, lens()), type = "l", lty = 2, ylab = "relative radius") lines(theta, .fp(theta, lens("Nikon_FCE9")))
Rescale numeric or raster data from an expected range to the range .
normalize_minmax(r, mn = NULL, mx = NULL, clip = FALSE)normalize_minmax(r, mn = NULL, mx = NULL, clip = FALSE)
r |
numeric terra::SpatRaster or numeric vector. Input data. |
mn |
numeric vector of length one or |
mx |
numeric vector of length one or |
clip |
logical vector of length one. If |
Same properties as r, with values rescaled to the range if
mn and mx match the range of r or extend beyond it. If clip = TRUE,
values will be within even if this implies data loss.
normalize_minmax(read_caim())normalize_minmax(read_caim())
Robust binarization without parameter tuning.
ootb_bin( caim, z, a, m, parallel = TRUE, cores = NULL, logical = TRUE, leave_free = 1 )ootb_bin( caim, z, a, m, parallel = TRUE, cores = NULL, logical = TRUE, leave_free = 1 )
caim |
numeric terra::SpatRaster with three layers named
|
z |
terra::SpatRaster generated with |
a |
terra::SpatRaster generated with |
m |
logical terra::SpatRaster with one layer. A binary mask with
|
parallel |
logical vector of length one. If |
cores |
numeric vector of length one. Number of CPU cores to use when
|
logical |
logical vector of length one. Internally it is passed unchanged
to the argument |
leave_free |
numeric vector of length one. Number of CPU cores to leave
free when |
Runs a predefined pipeline that incrementally refines a binary sky mask by combining gradient-based enhancement, local thresholding, polar segmentation, and a spectral index sensitive to sunlit canopy.
Enhancement. Compute complementary gradients with
complementary_gradients() and build an enhancer that mixes the
strongest complementary response with the blue band:
mem <- mean(normalize_minmax(max(yellow_blue, red_cyan)), normalize_minmax(Blue^(1/2.2))).
Gamma correction (see invert_gamma_correction()) is applied to the blue band to
reduce sky brightness variability.
Local thresholding. Apply apply_by_direction() on mem
with method = "thr_isodata" to obtain an initial binary mask. Local
thresholding is required because background non-uniformity remains in the
enhanced image.
Cleanup. Remove isolated pixels and apply a one-pixel binary
dilation. This compensates small artifacts produced by band misalignment
resulting from the radiometric-first policy of read_caim_raw().
Polar quadtree segmentation. Segment this preclassification of
sky and non-sky pixels with polar_qtree() parameterized to yield circular
trapezoids never smaller than degrees and to minimize
segments with mixed classes.
Object-based image analysis.
Keep segments that contain between 10 and 90 percent of sky pixels.
For each kept segment, estimate a local sky reference as the maximum blue
value, use it to normalize per segment
(ratio <- Blue / sky_segment_max), interpret the normalization as the degree
of membership to the sky class, and then defuzzify with a fixed threshold
equal to 0.5.
Logical terra::SpatRaster (TRUE for sky, FALSE for non-sky)
with the same number of rows and columns as caim.
This function is part of a paper under preparation.
## Not run: caim <- read_caim() r <- caim$Blue z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) m <- !is.na(z) bin <- ootb_bin(caim, z, a, m) plot(bin) ## End(Not run)## Not run: caim <- read_caim() r <- caim$Blue z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) m <- !is.na(z) bin <- ootb_bin(caim, z, a, m) plot(bin) ## End(Not run)
Fit a CIE general sky model to data obtained from a canopy image with an histogram-based method.
ootb_cie_model( r, z, a, m, method = c("Nelder-Mead", "BFGS", "CG", "SANN"), parallel = TRUE, cores = NULL, leave_free = 1, logical = TRUE )ootb_cie_model( r, z, a, m, method = c("Nelder-Mead", "BFGS", "CG", "SANN"), parallel = TRUE, cores = NULL, leave_free = 1, logical = TRUE )
r |
numeric terra::SpatRaster of one layer. Typically, the blue
band of a a canopy photograph. Digital numbers should be linearly related
to radiance. See |
z |
terra::SpatRaster generated with |
a |
terra::SpatRaster generated with |
m |
logical terra::SpatRaster with one layer. A binary mask with
|
method |
character vector. Optimization methods for |
parallel |
logical vector of length one. If |
cores |
numeric vector of length one. Number of CPU cores to use when
|
leave_free |
numeric vector of length one. Number of CPU cores to leave
free when |
logical |
logical vector of length one. Internally it is passed unchanged
to the argument |
List with the following components:
rrThe input rr with an added pred column in
sky_points, containing predicted values.
opt_resultList returned by stats::optim().
coefNumeric vector of length five. CIE model coefficients.
sun_anglesNumeric vector of length two. Sun zenith and azimuth (degrees).
methodCharacter vector of length one. Optimization method used.
startNumeric vector of length five. Starting parameters.
metricNumeric value. Mean squared deviation as in Gauch et al. (2003).
method_sunCharacter vector of length one or NULL.
Method used to optimice sun angles.
This function is part of a paper under preparation.
## Not run: caim <- read_caim() r <- caim$Blue z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) m <- !is.na(z) model <- ootb_cie_model(r, z, a, m) plot(model$rr$sky_points$rr, model$rr$sky_points$pred) abline(0,1) lm(model$rr$sky_points$pred~model$rr$sky_points$rr) %>% summary() sky <- cie_image(z, a, model$sun_angles, model$coef) * model$rr$zenith_dn plot(sky) ratio <- r/sky plot(ratio) plot(ratio > 1.05) plot(ratio > 1.15) ## End(Not run)## Not run: caim <- read_caim() r <- caim$Blue z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) m <- !is.na(z) model <- ootb_cie_model(r, z, a, m) plot(model$rr$sky_points$rr, model$rr$sky_points$pred) abline(0,1) lm(model$rr$sky_points$pred~model$rr$sky_points$rr) %>% summary() sky <- cie_image(z, a, model$sun_angles, model$coef) * model$rr$zenith_dn plot(sky) ratio <- r/sky plot(ratio) plot(ratio > 1.05) plot(ratio > 1.15) ## End(Not run)
Generate an above‑canopy sky brightness map without manual tuning.
ootb_sky_above(sky_points, z, a, sky_cie, size = 100)ootb_sky_above(sky_points, z, a, sky_cie, size = 100)
sky_points |
|
z |
terra::SpatRaster generated with |
a |
terra::SpatRaster generated with |
sky_cie |
list. Output of |
size |
numeric vector of length one. Number of rows and columns of the low-resolution grid used before resampling to full resolution. |
Interpolates sky brightness with IDW and k‑nearest neighbors in spherical
space via interpolate_spherical(), blending observations with a fitted sky
model. Blending and IDW parameters are derived from sky_cie validation
metrics, and the result is scaled by the modeled zenith value to yield
digital numbers.
Named list with:
dn_rasternumeric terra::SpatRaster with interpolated above‑canopy sky brightness in digital numbers.
wnumeric. Weight assigned to the model‑based filling source.
kinteger. Number of nearest neighbors used by IDW.
pnumeric. IDW power parameter.
This function is part of a paper under preparation.
## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) path <- system.file("external/example.txt", package = "rcaiman") sky_cie <- read_sky_cie(gsub(".txt", "", path), caim$Blue, z, a) sky_points <- sky_cie$model$rr$sky_points sky_above <- ootb_sky_above(sky_points, z, a, sky_cie) plot(sky_above$dn_raster) plot(caim$Blue/sky_above$dn_raster) ## End(Not run)## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) path <- system.file("external/example.txt", package = "rcaiman") sky_cie <- read_sky_cie(gsub(".txt", "", path), caim$Blue, z, a) sky_points <- sky_cie$model$rr$sky_points sky_above <- ootb_sky_above(sky_points, z, a, sky_cie) plot(sky_above$dn_raster) plot(caim$Blue/sky_above$dn_raster) ## End(Not run)
Fit and validate a CIE general sky model to radiance sampled on canopy gaps and return the predicted raster.
ootb_sky_cie( r, z, a, m, bin, equalarea_seg, seg, dist_to_black, method = c("Nelder-Mead", "BFGS", "CG", "SANN"), sun_disk_mode = c("obscured", "veiled", "twilight"), custom_sky_coef = NULL, std_sky_no = NULL, general_sky_type = NULL, optim_zenith_dn = TRUE, parallel = TRUE, cores = NULL, logical = TRUE, leave_free = 1 )ootb_sky_cie( r, z, a, m, bin, equalarea_seg, seg, dist_to_black, method = c("Nelder-Mead", "BFGS", "CG", "SANN"), sun_disk_mode = c("obscured", "veiled", "twilight"), custom_sky_coef = NULL, std_sky_no = NULL, general_sky_type = NULL, optim_zenith_dn = TRUE, parallel = TRUE, cores = NULL, logical = TRUE, leave_free = 1 )
r |
numeric terra::SpatRaster of one layer. Typically the blue band of a canopy photograph. Digital numbers should be linearly related to radiance. |
z |
terra::SpatRaster generated with |
a |
terra::SpatRaster generated with |
m |
logical terra::SpatRaster with one layer. A binary mask with
|
bin |
logical terra::SpatRaster with one layer. A binarized
hemispherical image. See |
equalarea_seg |
terra::SpatRaster generated with
|
seg |
single-layer terra::SpatRaster. Segmentation map of r,
typically created with functions such as |
dist_to_black |
numeric vector of length one or |
method |
character vector. Optimization methods passed to
|
sun_disk_mode |
character vector. Estimation modes for the solar disk.
Supported values are |
custom_sky_coef |
numeric vector of length five, or numeric matrix with five columns. Custom starting coefficients for optimization. If not provided, coefficients are initialized from standard skies. |
std_sky_no |
numeric vector. Standard sky numbers as in cie_table. If not provided, all are used. |
general_sky_type |
character vector of length one. Must be |
optim_zenith_dn |
logical vector of length one. See |
parallel |
logical vector of length one. If |
cores |
numeric vector of length one. Number of CPU cores to use when
|
logical |
logical vector of length one. Internally it is passed unchanged
to the argument |
leave_free |
numeric vector of length one. Number of CPU cores to leave
free when |
List with:
rrnumeric terra::SpatRaster. Predicted relative radiance.
modellist returned by fit_cie_model(). The optimal fit.
paramslist returned by tune_sky_sampling().
sky_pointsdata.frame with columns row and col. Locations of
sky points.
sun_row_coldata.frame with the estimated sun‑disk position in
image coordinates.
optimal_startcustom_sky_coef with the estimated sun‑disk position in
image coordinates.
This function is part of a paper under preparation.
## Not run: caim <- read_caim() r <- caim$Blue z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) m <- !is.na(z) com <- complementary_gradients(caim) thr <- thr_twocorner(com$red_cyan[]) bin <- binarize_with_thr(com$red_cyan, thr$uc) bin <- rem_isolated_black_pixels(bin) bin_list <- c(bin_list, bin) bin_list <- c(bin_list, rem_small_gaps(bin)) set.seed(7) sky_cie <- ootb_sky_cie(r, z, a, m, bin_list, n_cells_seq, dist_to_black_seq, method = c("Nelder-Mead", "BFGS", "CG", "SANN"), twilight = FALSE, std_sky_no = 12, optim_zenith_dn = TRUE, parallel = TRUE) sky_cie$rr plot(sky_cie$rr) plot(sky_cie$model$rr$sky_points$pred, sky_cie$model$rr$sky_points$rr, xlab = "Predicted relative radiance", ylab = "Observed relative radiance") abline(0,1) ratio <- r/sky_cie$rr/sky_cie$model$rr$zenith_dn plot(ratio) plot(select_sky_region(ratio, 0.95, 1.05)) plot(select_sky_region(ratio, 1.05, 100)) display_caim(caim, sampling_points = sky_cie$sky_points, sun_row_col = sun) ## End(Not run)## Not run: caim <- read_caim() r <- caim$Blue z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) m <- !is.na(z) com <- complementary_gradients(caim) thr <- thr_twocorner(com$red_cyan[]) bin <- binarize_with_thr(com$red_cyan, thr$uc) bin <- rem_isolated_black_pixels(bin) bin_list <- c(bin_list, bin) bin_list <- c(bin_list, rem_small_gaps(bin)) set.seed(7) sky_cie <- ootb_sky_cie(r, z, a, m, bin_list, n_cells_seq, dist_to_black_seq, method = c("Nelder-Mead", "BFGS", "CG", "SANN"), twilight = FALSE, std_sky_no = 12, optim_zenith_dn = TRUE, parallel = TRUE) sky_cie$rr plot(sky_cie$rr) plot(sky_cie$model$rr$sky_points$pred, sky_cie$model$rr$sky_points$rr, xlab = "Predicted relative radiance", ylab = "Observed relative radiance") abline(0,1) ratio <- r/sky_cie$rr/sky_cie$model$rr$zenith_dn plot(ratio) plot(select_sky_region(ratio, 0.95, 1.05)) plot(select_sky_region(ratio, 1.05, 100)) display_caim(caim, sampling_points = sky_cie$sky_points, sun_row_col = sun) ## End(Not run)
Refine the solar position in a fitted CIE sky model by optimizing zenith and azimuth to best match observed relative radiance.
optim_sun_angles(model, method = c("Nelder-Mead", "BFGS", "CG", "SANN"))optim_sun_angles(model, method = c("Nelder-Mead", "BFGS", "CG", "SANN"))
model |
list returned by |
method |
character vector. One or more optimization methods supported by
|
Evaluates one or more methods from stats::optim() starting at
model$sun_angles. After each optimization the model is re‑fitted and the
process repeats until the change in solar position is < 1 deg. The best
result across methods is kept.
List like model, potentially with updated sun_angles and
metric, and a new method_sun indicating the best optimization method.
If no improvement is found, method_sun is NULL.
The objective function penalizes solutions that move the sun position by more than 10 deg from the initial estimate to discourage unrealistic shifts.
## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) # See fit_cie_model() for details on below file path <- system.file("external/sky_points.csv", package = "rcaiman") sky_points <- read.csv(path) sky_points <- sky_points[c("Y", "X")] colnames(sky_points) <- c("row", "col") sun_angles <- c(z = 39.5, a = 17.4) rr <- extract_rr(caim$Blue, z, a, sky_points) set.seed(7) model <- fit_cie_model(rr, sun_angles, general_sky_type = "Clear") print(model$sun_angles) print(model$metric) plot(model$rr$sky_points$rr, model$rr$sky_points$pred) abline(0,1) lm(model$rr$sky_points$pred~model$rr$sky_points$rr) %>% summary() model <- optim_sun_angles(model) print(model$sun_angles) print(model$metric) model$method_sun ## End(Not run)## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) # See fit_cie_model() for details on below file path <- system.file("external/sky_points.csv", package = "rcaiman") sky_points <- read.csv(path) sky_points <- sky_points[c("Y", "X")] colnames(sky_points) <- c("row", "col") sun_angles <- c(z = 39.5, a = 17.4) rr <- extract_rr(caim$Blue, z, a, sky_points) set.seed(7) model <- fit_cie_model(rr, sun_angles, general_sky_type = "Clear") print(model$sun_angles) print(model$metric) plot(model$rr$sky_points$rr, model$rr$sky_points$pred) abline(0,1) lm(model$rr$sky_points$pred~model$rr$sky_points$rr) %>% summary() model <- optim_sun_angles(model) print(model$sun_angles) print(model$metric) model$method_sun ## End(Not run)
Evaluate a sequence of candidate minimum distances and select the one that minimizes the Kullback–Leibler (KL) divergence from a uniform distribution on an equal-area hemispherical segmentation.
optimize_sampling_uniformity(sky_points, r, z, a, equalarea_seg, min_dist_seq)optimize_sampling_uniformity(sky_points, r, z, a, equalarea_seg, min_dist_seq)
sky_points |
|
r |
terra::SpatRaster. Image from which the points were sampled (or any raster with identical dimensions). |
z |
terra::SpatRaster generated with |
a |
terra::SpatRaster generated with |
equalarea_seg |
terra::SpatRaster generated with |
min_dist_seq |
numeric vector. Candidate minimum distances (deg). |
For each value in min_dist_seq, the input point pattern (sky_points) is
thinned using rem_nearby_points(). The resulting points are assigned to the
cells of equalarea_seg, and their empirical distribution is compared with
the uniform reference through KL divergence, as computed by
assess_sampling_uniformity(). As a results, it improves spatial regularity
by selecting an optimal minimum distance.
List with:
sky_pointsThinned point set using the optimal distance.
min_distValue in min_dist_seq that minimized KL divergence.
kl_valuesKL divergence for each candidate distance.
## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) m <- !is.na(z) r <- caim$Blue bin <- binarize_by_region(r, ring_segmentation(z, 15), "thr_isodata") & select_sky_region(z, 0, 88) seg <- skygrid_segmentation(z, a, 10) sky_points <- sample_sky_points(r, bin, seg, dist_to_black = 3) plot(bin) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) l <- optimize_sampling_uniformity(sky_points, NULL, z, a, equalarea_segmentation(z, a, 100), 0:15) plot(l$kl_values) l$min_dist plot(bin) points(l$sky_points$col, nrow(caim) - l$sky_points$row, col = 2, pch = 10) ## End(Not run)## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) m <- !is.na(z) r <- caim$Blue bin <- binarize_by_region(r, ring_segmentation(z, 15), "thr_isodata") & select_sky_region(z, 0, 88) seg <- skygrid_segmentation(z, a, 10) sky_points <- sample_sky_points(r, bin, seg, dist_to_black = 3) plot(bin) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) l <- optimize_sampling_uniformity(sky_points, NULL, z, a, equalarea_segmentation(z, a, 100), 0:15) plot(l$kl_values) l$min_dist plot(bin) points(l$sky_points$col, nrow(caim) - l$sky_points$row, col = 2, pch = 10) ## End(Not run)
Paint image pixels inside or outside a logical mask with a solid color.
paint_with_mask(r, m, color = "red", where = "outside")paint_with_mask(r, m, color = "red", where = "outside")
r |
terra::SpatRaster. The image. Values should be normalized,
see |
m |
logical terra::SpatRaster with one layer. A binary mask with
|
color |
character vector of length one or numeric vector of length
three. Fill color. If character, it is converted to RGB automatically. If
numeric, values must be in range |
where |
character vector of length one Region to paint relative to |
numeric terra::SpatRaster with three layers and the geometry
of r. Equal to r, but with pixels in the selected region painted with
color. Single-layer inputs are replicated to allow color painting.
## Not run: r <- read_caim() z <- zenith_image(ncol(r), lens()) a <- azimuth_image(z) m <- select_sky_region(z, 20, 70) & select_sky_region(a, 90, 180) masked_caim <- paint_with_mask(normalize_minmax(r), m) plotRGB(masked_caim * 255) masked_bin <- paint_with_mask(binarize_with_thr(r$Blue, 125), m) plotRGB(masked_bin * 255) r <- normalize_minmax(r) paint_with_mask(r, m, color = c(0.2, 0.2, 0.2)) # vector paint_with_mask(r, m, color = "blue") # name paint_with_mask(r, m, color = "#00FF00") # hexadecimal ## End(Not run)## Not run: r <- read_caim() z <- zenith_image(ncol(r), lens()) a <- azimuth_image(z) m <- select_sky_region(z, 20, 70) & select_sky_region(a, 90, 180) masked_caim <- paint_with_mask(normalize_minmax(r), m) plotRGB(masked_caim * 255) masked_bin <- paint_with_mask(binarize_with_thr(r$Blue, 125), m) plotRGB(masked_bin * 255) r <- normalize_minmax(r) paint_with_mask(r, m, color = c(0.2, 0.2, 0.2)) # vector paint_with_mask(r, m, color = "blue") # name paint_with_mask(r, m, color = "#00FF00") # hexadecimal ## End(Not run)
Segment a hemispherical image into large circular trapezoids and recursively split them into four trapezoids of equal angular size whenever brightness heterogeneity exceeds a predefined threshold.
polar_qtree(r, z, a, scale_parameter, angle_width = 30, max_splittings = 6)polar_qtree(r, z, a, scale_parameter, angle_width = 30, max_splittings = 6)
r |
numeric terra::SpatRaster. One or more layers used to drive heterogeneity. |
z |
terra::SpatRaster generated with |
a |
terra::SpatRaster generated with |
scale_parameter |
numeric vector of length one. Threshold on |
angle_width |
numeric vector of length one. Angle in deg that must
divide both 0–360 and 0–90 into an integer number of segments. Retrieve a
set of valid values by running
|
max_splittings |
numeric vector of length one. Maximum recursion depth. |
A circular trapezoid, hereafter referred to as a cell, is the
intersection of a ring (zenith‑angle band) and a sector (azimuth‑angle band).
Heterogeneity within a cell is measured as the standard deviation of pixel
values (a first‑order texture metric). The change in heterogeneity due to
splitting is delta, defined as the sum of the standard deviations of the
four subcells minus the standard deviation of the parent cell. A split is
kept where delta > scale_parameter. For multi‑layer r, delta is
computed per layer and averaged to decide splits. Angular resolution at level
i is angle_width / 2^i.
Single-layer terra::SpatRaster with integer values and the
same number of rows and columns as r.
## Not run: # Find large patches of white --------------------------------------------- caim <- read_caim() r <- caim$Blue z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) bin <- binarize_with_thr(r, thr_isodata(r[])) plot(bin) seg <- polar_qtree(bin, z, a, 0, 30, 3) plot(extract_feature(bin, seg) == 1) ## End(Not run)## Not run: # Find large patches of white --------------------------------------------- caim <- read_caim() r <- caim$Blue z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) bin <- binarize_with_thr(r, thr_isodata(r[])) plot(bin) seg <- polar_qtree(bin, z, a, 0, 30, 3) plot(extract_feature(bin, seg) == 1) ## End(Not run)
Wrapper functions around terra::rast() to read and write binary masks.
read_bin(path) write_bin(bin, path)read_bin(path) write_bin(bin, path)
path |
character vector of length one. File path to read or write. See examples. |
bin |
logical terra::SpatRaster with a single layer. |
write_bin() multiplies the input logical raster by 255 and writes the
result as a GeoTIFF (GTiff) with datatype INT1U. Both write_bin() and
read_bin() set the raster extent to terra::ext(0, ncol(r), 0, nrow(r))
and the CRS to EPSG:7589 (see set_rcaiman_geometry()).
See Functions
write_binWrite a one-layer logical terra::SpatRaster
to disk as a GeoTIFF (GTiff, INT1U). No return value.
read_binRead a file with values 255 and/or 0, such as the one
produced by write_bin (see Details), and return a logical
terra::SpatRaster (TRUE for 255, FALSE for 0).
## Not run: z <- zenith_image(1000, lens()) m <- !is.na(z) my_file <- file.path(tempdir(), "mask.tif") write_bin(m, my_file) m_from_disk <- read_bin(my_file) plot(m - m_from_disk) ## End(Not run)## Not run: z <- zenith_image(1000, lens()) m <- !is.na(z) my_file <- file.path(tempdir(), "mask.tif") write_bin(m, my_file) m_from_disk <- read_bin(my_file) plot(m - m_from_disk) ## End(Not run)
Reads a born-digital image (typically RGB-JPEG or RGB-TIFF) using
terra::rast() and returns a terra::SpatRaster object.
read_caim(path = NULL, zenith_colrow = NULL, horizon_radius = NULL)read_caim(path = NULL, zenith_colrow = NULL, horizon_radius = NULL)
path |
character vector of length one. Path to an image file, including
extension. If |
zenith_colrow |
numeric vector of length 2. Column and row coordinates of the optical center (zenith) in pixel units. |
horizon_radius |
integer-like numeric vector of length one. Radius of the
hemispherical image in pixel units, measured from the optical center
( |
Internally, this is a wrapper around terra::rast(), so support for image
formats depends on the capabilities of the terra package.
This function is intended for importing color hemispherical photographs, such
as those obtained with digital cameras equipped with fisheye lenses. For raw
image files (e.g., NEF, CR2), see read_caim_raw(). A typical workflow makes
use of read_caim_raw(), write_caim(), and read_caim()
The example image was created from a raw photograph taken with a Nikon Coolpix 5700 and a FC-E9 auxiliary lens, processed with the following code:
zenith_colrow <- c(1290, 988)/2
diameter <- 756
z <- zenith_image(diameter, lens("Nikon_FCE9"))
a <- azimuth_image(z)
m <- !is.na(z)
caim <- read_caim_raw("DSCN4606.NEF")
caim <- crop_caim(caim, zenith_colrow - diameter/2, diameter, diameter)
caim <- correct_vignetting(caim, z, c(0.0638, -0.101))
caim <- c(mean(caim$Y, caim$M), caim$G, caim$C)
caim <- fisheye_to_equidistant(caim, z, a, m, radius = 300, k = 1)
write_caim(caim, "example.tif", 16)
Numeric terra::SpatRaster, typically with layers named
"Red", "Green", and "Blue". If the file extension does not correspond
to typical JPEG or TIFF files, names will be inferred and a
warning will be issued.
rcaiman uses terra without geographic semantics: rasters are kept with
unit resolution (cell size = 1) and a standardized extent
ext(0, ncol, 0, nrow) with CRS EPSG:7589 (set_rcaiman_geometry()).
path <- system.file("external/DSCN4500.JPG", package = "rcaiman") zenith_colrow <- c(1286, 986) horizon_radius <- 742 caim <- read_caim(path, zenith_colrow, horizon_radius) plot(caim$Blue)path <- system.file("external/DSCN4500.JPG", package = "rcaiman") zenith_colrow <- c(1286, 986) horizon_radius <- 742 caim <- read_caim(path, zenith_colrow, horizon_radius) plot(caim$Blue)
Read unprocessed sensor data from a camera RAW file and split the signal by spectral band according to the in-camera color filter array (CFA). Use this to obtain images with precise radiometry.
read_caim_raw( path, only_blue = FALSE, offset_value = NULL, cfa_pattern = NULL, spectral_mapping = NULL )read_caim_raw( path, only_blue = FALSE, offset_value = NULL, cfa_pattern = NULL, spectral_mapping = NULL )
path |
character vector of length one. Path to a file with raw data (including file extension). |
only_blue |
logical vector of length one. If |
offset_value |
numeric vector of length one. Optional black level offsets to replace
|
cfa_pattern |
character matrix of two rows by two columns. Declares the
ordered set of color filter elements used by the sensor. Values should
correspond to semantic color labels (e.g. |
spectral_mapping |
optional named list. Declares how target spectral
bands should be derived from sensor channels. This argument is intended for
use with |
Uses Python rawpy through reticulate
to access sensor data and black-level metadata. Optionally extracts only the
blue/cyan band.
Numeric terra::SpatRaster:
single-layer if only_blue = TRUE.
multi-layer if only_blue = FALSE, with one layer per color per CFA
color (e.g., R, G, B).
Layers are named according to metadata in the raw file.
To ensure that R can access a Python installation, run the following test:
reticulate::py_eval("1+1")
If R can access Python successfully, you will see 2 in the console. If not,
you will receive instructions on how to install Python.
Hint: If you are temporarily offline, try setting Sys.setenv(UV_OFFLINE=1).
Run reticulate::py_last_error() for details. Try to load rawpy explicitly.
After passing the Python accessibility test, create a virtual environment using the following command:
reticulate::virtualenv_create()
rawpy
Install the rawpy package within the virtual environment:
reticulate::py_install("rawpy")
If you are an RStudio user who works with projects, you will need a .Renviron file in the root of each project. To create a .Renviron file, follow these steps:
Create a "New Blank File" named ".Renviron" (without an extension) in the project's root directory.
Run bellow code:
path <- file.path(reticulate::virtualenv_root(),
reticulate::virtualenv_list(), "Scripts", "python.exe")
paste("RETICULATE_PYTHON =", path)
Copy/paste the line from the console (the string between the quotes) into
the .Renviron file. This is an example RETICULATE_PYTHON = ~/.virtualenvs/r-reticulate/Scripts/python.exe
Do not forget to save the changes
By following these steps, users can easily set up their environment to access raw data efficiently, but it is not the only way of doing it, you might know an easier or better one.
See the help page of read_caim() and fisheye_to_equidistant() as a
complement to this help page. Further details about raw files can be found in
Díaz et al. (2024).
Díaz GM, Lang M, Kaha M (2024). “Simple calibration of fisheye lenses for hemipherical photography of the forest canopy.” Agricultural and Forest Meteorology, 352, 110020. ISSN 0168-1923. doi:10.1016/j.agrformet.2024.110020.
## Not run: file_name <- tempfile(fileext = ".NEF") download.file("https://osf.io/s49py/download", file_name, mode = "wb") # Geometric and radiometric corrections ----------------------------------- zenith_colrow <- c(1290, 988)/2 diameter <- 756 z <- zenith_image(diameter, lens("Nikon_FCE9")) a <- azimuth_image(z) m <- !is.na(z) caim <- read_caim_raw(file_name, only_blue = TRUE) caim <- crop_caim(caim, zenith_colrow - diameter/2, diameter, diameter) caim <- correct_vignetting(caim, z, c(0.0638, -0.101)) caim <- fisheye_to_equidistant(caim, z, a, m, radius = 300, k = 1, p = 1, rmax = 100) ## End(Not run)## Not run: file_name <- tempfile(fileext = ".NEF") download.file("https://osf.io/s49py/download", file_name, mode = "wb") # Geometric and radiometric corrections ----------------------------------- zenith_colrow <- c(1290, 988)/2 diameter <- 756 z <- zenith_image(diameter, lens("Nikon_FCE9")) a <- azimuth_image(z) m <- !is.na(z) caim <- read_caim_raw(file_name, only_blue = TRUE) caim <- crop_caim(caim, zenith_colrow - diameter/2, diameter, diameter) caim <- correct_vignetting(caim, z, c(0.0638, -0.101)) caim <- fisheye_to_equidistant(caim, z, a, m, radius = 300, k = 1, p = 1, rmax = 100) ## End(Not run)
Replace single black pixels (FALSE) that are fully surrounded by white
pixels (TRUE) with white. Uses 8-connectivity.
rem_isolated_black_pixels(bin)rem_isolated_black_pixels(bin)
bin |
logical terra::SpatRaster with a single layer. |
Logical terra::SpatRaster of one layer.
## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) r <- caim$Blue bin <- binarize_by_region(r, ring_segmentation(z, 15), "thr_isodata") plot(bin) plot(rem_isolated_black_pixels(bin)) ## End(Not run)## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) r <- caim$Blue bin <- binarize_by_region(r, ring_segmentation(z, 15), "thr_isodata") plot(bin) plot(rem_isolated_black_pixels(bin)) ## End(Not run)
Select a subset of points so that no retained pair is closer than min_dist
in planar or spherical space.
rem_nearby_points( sky_points, r = NULL, z = NULL, a = NULL, min_dist = 3, space = "planar", use_window = TRUE )rem_nearby_points( sky_points, r = NULL, z = NULL, a = NULL, min_dist = 3, space = "planar", use_window = TRUE )
sky_points |
|
r |
single-layer terra::SpatRaster or |
z |
terra::SpatRaster generated with |
a |
terra::SpatRaster generated with |
min_dist |
numeric vector of length one. Minimum allowed distance
between retained points. Units: pixels for |
space |
character vector of length one. Coordinate system for distances:
|
use_window |
logical vector of length one. If |
When space = "planar", distances are computed in image coordinates and z
and a are ignored. When space = "spherical", distances are angular (deg)
in hemispherical coordinates. If r is provided, points are ranked by the
extracted raster values and retained in descending order.
A data.frame with columns row and col for retained points.
It is assumed that sky_points were extracted from an image with the same
dimensions as the r, z, and a rasters. No checks are performed.
## Not run: caim <- read_caim() r <- caim$Blue z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) bin <- binarize_by_region(r, ring_segmentation(z, 30), method = "thr_isodata") bin <- bin & select_sky_region(z, 0, 80) g <- skygrid_segmentation(z, a, 10, first_ring_different = TRUE) sky_points <- sample_sky_points(r, bin, g, dist_to_black = 3) # planar sky_points_p <- rem_nearby_points(sky_points, r, min_dist = 100, space = "planar") plot(r) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) points(sky_points_p$col, nrow(caim) - sky_points_p$row, col = 3, pch = 0) # spherical sky_points_s <- rem_nearby_points(sky_points, r, z, a, min_dist = 30, space = "spherical") plot(r) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) points(sky_points_s$col, nrow(caim) - sky_points_s$row, col = 3, pch = 0) ## End(Not run)## Not run: caim <- read_caim() r <- caim$Blue z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) bin <- binarize_by_region(r, ring_segmentation(z, 30), method = "thr_isodata") bin <- bin & select_sky_region(z, 0, 80) g <- skygrid_segmentation(z, a, 10, first_ring_different = TRUE) sky_points <- sample_sky_points(r, bin, g, dist_to_black = 3) # planar sky_points_p <- rem_nearby_points(sky_points, r, min_dist = 100, space = "planar") plot(r) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) points(sky_points_p$col, nrow(caim) - sky_points_p$row, col = 3, pch = 0) # spherical sky_points_s <- rem_nearby_points(sky_points, r, z, a, min_dist = 30, space = "spherical") plot(r) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) points(sky_points_s$col, nrow(caim) - sky_points_s$row, col = 3, pch = 0) ## End(Not run)
Remove sky points considered outliers relative to their local neighbors in a user-specified variable.
rem_outliers( sky_points, r, z, a, k = 20, angular_radius = 20, laxity = 2, cutoff_side = "both", use_window = TRUE, detrend = FALSE, parallel = FALSE, cores = NULL, logical = TRUE, leave_free = 1 )rem_outliers( sky_points, r, z, a, k = 20, angular_radius = 20, laxity = 2, cutoff_side = "both", use_window = TRUE, detrend = FALSE, parallel = FALSE, cores = NULL, logical = TRUE, leave_free = 1 )
sky_points |
|
r |
terra::SpatRaster. Image from which the points were sampled (or any raster with identical dimensions). |
z |
terra::SpatRaster generated with |
a |
terra::SpatRaster generated with |
k |
numeric vector of length one. Number of neighbors. |
angular_radius |
numeric vector of length one. The maximum radius for searching k-nearest neighbors (KNN) in degrees. |
laxity |
numeric vector of length one. Positive multiplier applied to
MAD to set classification thresholds. Default is |
cutoff_side |
character vector of length one. One of |
use_window |
logical vector of length one. If |
detrend |
logical vector of length one. When |
parallel |
logical vector of length one. If |
cores |
numeric vector of length one. Number of CPU cores to use when
|
logical |
logical vector of length one. Internally it is passed unchanged
to the argument |
leave_free |
numeric vector of length one. Number of CPU cores to leave
free when |
Based on the Statistical Outlier Removal (SOR) filter from the
PCL library. Distances are computed on a spherical
surface. The number of neighbors is controlled by k, and angular_radius
sets the maximum search radius (deg). If fewer than k neighbors are found
within that radius, the point is retained due to insufficient evidence for
removal. The decision criterion is from is_outlier().
The retained points represented as a data.frame with columns row
and col, same as sky_points.
This function assumes that sky_points and the
terra::SpatRaster objects refer to the same image geometry. No checks
are performed.
## Not run: caim <- read_caim() r <- caim$Blue z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) m <- !is.na(z) bin <- binarize_by_region(r, ring_segmentation(z, 30), method = "thr_isodata") bin <- bin & select_sky_region(z, 0, 80) seg <- equalarea_segmentation(z, a, 1000) sky_points <- sample_sky_points(r, bin, seg, dist_to_black = 3) plot(r) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) sky_points2 <- rem_outliers(sky_points, r, z, a, k = 20, angular_radius = 30, laxity = 2, cutoff_side = "left", detrend = TRUE) points(sky_points2$col, nrow(caim) - sky_points2$row, col = 3, pch = 0) ## End(Not run)## Not run: caim <- read_caim() r <- caim$Blue z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) m <- !is.na(z) bin <- binarize_by_region(r, ring_segmentation(z, 30), method = "thr_isodata") bin <- bin & select_sky_region(z, 0, 80) seg <- equalarea_segmentation(z, a, 1000) sky_points <- sample_sky_points(r, bin, seg, dist_to_black = 3) plot(r) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) sky_points2 <- rem_outliers(sky_points, r, z, a, k = 20, angular_radius = 30, laxity = 2, cutoff_side = "left", detrend = TRUE) points(sky_points2$col, nrow(caim) - sky_points2$row, col = 3, pch = 0) ## End(Not run)
Remove patches of connected TRUE pixels based on a white top-hat pixel-based
morphological filter or an object-based fixed area/size threshold.
rem_small_gaps(bin, size = 9, method = "pixel-based")rem_small_gaps(bin, size = 9, method = "pixel-based")
bin |
logical terra::SpatRaster with one layer. A binarized
hemispherical image. See |
size |
numeric vector of length one. See Details. |
method |
character vector of length one. See Details. |
Methods provided:
Internally runs EBImage::whiteTopHat() with a kernel
of size equal to size and diamond shape.
Patches of connected TRUE pixes (canopy gaps) are turn FALSE if they are
rounded but too small, or if they are of a shape and size unlikely for a
real canopy gap. The function internally uses EBImage for calculates the
area () and radii standard deviation () of canopy gaps. While
is simply the total number of pixels of the segment, is the standard
deviation of the Euclidean distance between the gap center and the pixels
that are in the gap contour. The center is computed as the average of the
coordinates of the contour pixels. The effective circular area equation is
, providing a size metric that accounts for deviations from
circularity. With this method, argument size is a threshold of ECA for
recognizing small gaps.
Logical terra::SpatRaster with the same dimensions as bin.
## Not run: r <- read_caim() bin <- binarize_with_thr(r$Blue, thr_isodata(r$Blue[])) plot(bin) bin <- rem_small_gaps(bin, 5, method = "pixel-based") plot(bin) bin <- rem_small_gaps(bin, 11, method = "object-based") plot(bin) ## End(Not run)## Not run: r <- read_caim() bin <- binarize_with_thr(r$Blue, thr_isodata(r$Blue[])) plot(bin) bin <- rem_small_gaps(bin, 5, method = "pixel-based") plot(bin) bin <- rem_small_gaps(bin, 11, method = "object-based") plot(bin) ## End(Not run)
Segment a hemispherical view into concentric rings by slicing the zenith
angle from 0 to 90 deg at equal steps.
ring_segmentation(z, angle_width, return = "id")ring_segmentation(z, angle_width, return = "id")
z |
terra::SpatRaster generated with |
angle_width |
numeric vector of length one. Ring width in degrees. Must divide the 0-90 deg range into an integer number of segments. |
return |
character vector of length one. Output mode: "id" (default) or "angle". |
Single-layer terra::SpatRaster: ring IDs if return = "id",
or mean zenith angle (deg) if return = "angle".
z <- zenith_image(600, lens()) rings <- ring_segmentation(z, 15) plot(rings == 1)z <- zenith_image(600, lens()) rings <- ring_segmentation(z, 15) plot(rings == 1)
Sample representative sky pixels for use in model fitting or interpolation.
sample_sky_points(r, bin, seg, dist_to_black = 3, method = "per_cell")sample_sky_points(r, bin, seg, dist_to_black = 3, method = "per_cell")
r |
numeric terra::SpatRaster of one layer. Typically the blue band of a canopy image. |
bin |
logical terra::SpatRaster of one layer. Binary image where
|
seg |
single-layer terra::SpatRaster. Segmentation map of r,
typically created with functions such as |
dist_to_black |
numeric vector of length one or |
method |
character vector of length one. Sampling method; either
|
Two sampling strategies are provided:
"per_cell"select one sky point per cell of a segmentation (seg)
as the brightest pixel marked TRUE in bin, provided the cell’s white
pixel count exceeds one fourth of the mean across valid cells.
"local_max"detect local maxima within a fixed
window, restricted to pixels marked TRUE in bin, after removing
patches of connected TRUE pixels that are implausible based on fixed
area/size thresholds. Each detected maximum is taken as a sky point.
Use "per_cell" to promote an even, representative spatial distribution (good
for model fitting), and "local_max" to be exhaustive for interpolation.
data.frame with columns row and col.
## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) r <- caim$Blue bin <- binarize_by_region(r, ring_segmentation(z, 15), "thr_isodata") & select_sky_region(z, 0, 88) seg <- skygrid_segmentation(z, a, 10) sky_points <- sample_sky_points(r, bin, seg, dist_to_black = 3) plot(bin) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) ## End(Not run)## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) r <- caim$Blue bin <- binarize_by_region(r, ring_segmentation(z, 15), "thr_isodata") & select_sky_region(z, 0, 88) seg <- skygrid_segmentation(z, a, 10) sky_points <- sample_sky_points(r, bin, seg, dist_to_black = 3) plot(bin) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) ## End(Not run)
Segment a hemispherical view into equal azimuth sectors by slicing the
azimuth angle from 0 to 360 deg at fixed steps.
sector_segmentation(a, angle_width)sector_segmentation(a, angle_width)
a |
terra::SpatRaster generated with |
angle_width |
numeric vector of lenght one. Sector width in degrees. Must divide the 0–360 deg range into an integer number of sectors. |
Single-layer terra::SpatRaster with integer values. Segments will resemble pizza slices.
z <- zenith_image(600, lens()) a <- azimuth_image(z) sectors <- sector_segmentation(a, 15) plot(sectors == 1)z <- zenith_image(600, lens()) a <- azimuth_image(z) sectors <- sector_segmentation(a, 15) plot(sectors == 1)
Select pixels from a single-layer image based on value limits.
select_sky_region(r, from, to)select_sky_region(r, from, to)
r |
single-layer terra::SpatRaster, typically from
|
from, to
|
numeric vectors of length one. Angles in deg, inclusive limits. |
Works with any numeric terra::SpatRaster of one layer, but is
especially well-suited for images from zenith_image() or
azimuth_image(). For azimuth ranges that wrap around 0 deg, combine two
masks with logical OR.
Logical terra::SpatRaster (TRUE for the selected region) of
the same dimensions as r.
## Not run: z <- zenith_image(1000, lens()) a <- azimuth_image(z) m1 <- select_sky_region(z, 20, 70) plot(m1) m2 <- select_sky_region(a, 330, 360) plot(m2) plot(m1 & m2) plot(m1 | m2) # 15 deg on each side of 0 m1 <- select_sky_region(a, 0, 15) m2 <- select_sky_region(a, 345, 360) plot(m1 | m2) # You can use this plot(!is.na(z)) # instead of this plot(select_sky_region(z, 0, 90)) ## End(Not run)## Not run: z <- zenith_image(1000, lens()) a <- azimuth_image(z) m1 <- select_sky_region(z, 20, 70) plot(m1) m2 <- select_sky_region(a, 330, 360) plot(m2) plot(m1 & m2) plot(m1 | m2) # 15 deg on each side of 0 m1 <- select_sky_region(a, 0, 15) m2 <- select_sky_region(a, 345, 360) plot(m1 | m2) # You can use this plot(!is.na(z)) # instead of this plot(select_sky_region(z, 0, 90)) ## End(Not run)
Apply the internal raster geometry convention used throughout rcaiman: rasters are treated as image grids, not as spatial objects.
set_rcaiman_geometry(r)set_rcaiman_geometry(r)
r |
numeric terra::SpatRaster. |
This function enforces a unit-resolution pixel grid with a
standardized extent ext(0, ncol, 0, nrow) and assigns a non-geographic CRS
as an explicit marker that no spatial semantics are intended. The EPSG:7589
was selected because it does not have datum, projection, or physical units.
This ensure consistent behavior across terra-based workflows. Using crs = NA is intentionally avoided since many terra methods and external tools
assume a defined CRS and may otherwise fail, emit warnings, or apply implicit
defaults.
The same terra::SpatRaster with standardized extent and CRS.
Return image row and column indices for the center point of each cell in a
sky grid composed of circular trapezoids of equal angular resolution defined
by angle_width such as in skygrid_segmentation().
skygrid_centers(z, a, angle_width)skygrid_centers(z, a, angle_width)
z |
terra::SpatRaster generated with |
a |
terra::SpatRaster generated with |
angle_width |
numeric vector of length one. Angle in deg that must
divide both 0–360 and 0–90 into an integer number of segments. Retrieve a
set of valid values by running
|
data.frame with integer columns row and col, one per grid cell.
z <- zenith_image(100, lens()) a <- azimuth_image(z) sampling_points <- skygrid_centers(z, a, 30) ## Not run: display_caim(is.na(z), sampling_points = sky_points) ## End(Not run)z <- zenith_image(100, lens()) a <- azimuth_image(z) sampling_points <- skygrid_centers(z, a, 30) ## Not run: display_caim(is.na(z), sampling_points = sky_points) ## End(Not run)
Segment a hemispherical view into equal-angle bins in both zenith and azimuth angles, assigning each pixel a grid-cell ID.
skygrid_segmentation(z, a, angle_width, first_ring_different = FALSE)skygrid_segmentation(z, a, angle_width, first_ring_different = FALSE)
z |
terra::SpatRaster generated with |
a |
terra::SpatRaster generated with |
angle_width |
numeric vector of length one. Angle in deg that must
divide both 0–360 and 0–90 into an integer number of segments. Retrieve a
set of valid values by running
|
first_ring_different |
logical vector of length one. If |
The intersection of zenith rings and azimuth sectors forms a grid whose cells
are circular trapezoids. By default, IDs encode both components as
sectorID * 1000 + ringID.
The code below outputs a comprehensive list of valid values for angle_width.
For convenience, the column radians_denom can be used to provide
angle_width as 180 / radians_denom_i, where radians_denom_i is a value
taken from radians_denom.
df <- data.frame(degrees = 90 / 1:180)
deg_to_pi_expr <- function(deg) {
frac <- MASS::fractions(deg / 180)
strsplit(as.character(frac), "/")[[1]][2] %>% as.numeric()
}
df$radians_denom <- sapply(df$degrees, function(deg) deg_to_pi_expr(deg))
z <- zenith_image(10, lens())
a <- azimuth_image(z)
u <- c()
for (i in 1:nrow(df)) {
u <- c(u, tryCatch(is((skygrid_segmentation(z, a,
180/df$radians_denom[i])), "SpatRaster"),
error = function(e) FALSE))
}
df <- df[u, ]
df
Single-layer terra::SpatRaster with integer labels. The
object carries attributes angle_width and first_ring_different.
caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) g <- skygrid_segmentation(z, a, 15) plot(g == 24005) ## Not run: display_caim(seg = g) ## End(Not run)caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) g <- skygrid_segmentation(z, a, 15) plot(g == 24005) ## Not run: display_caim(seg = g) ## End(Not run)
Verify that a lens projection maps zenith 0 deg to 0 and 90 to 1.
test_lens_coef(lens_coef)test_lens_coef(lens_coef)
lens_coef |
numeric vector. Polynomial coefficients of the lens
projection function. See |
The package tolerate a number very close to 1 at 90 deg but not exactly 1 as
long as it is greater than 1. See testthat::expect_equal() for tolerance
details.
When the test fails at Test that lens projection function does not predict values barely below one, the best practice is to manually edit the last coefficient (e.g., change -0.0296 to -0.0295).
If it fails at Test that lens projection function works within the 0–1 range, new calibration data may be required.
Invisibly returns TRUE if all checks pass; otherwise an error is
thrown.
test_lens_coef(lens("Nikon_FCE9")) test_lens_coef(2/pi)test_lens_coef(lens("Nikon_FCE9")) test_lens_coef(2/pi)
Compute a threshold using the IsoData algorithm Ridler and Calvard (1978), recommended by Jonckheere et al. (2005).
thr_isodata(x)thr_isodata(x)
x |
numeric vector or a single-column |
Implementation follows the IsoData method by Gabriel Landini, as implemented
in autothresholdr::auto_thresh(). Unlike that version, this function
accepts numeric data over an arbitrary range. NA values are ignored.
Numeric vector of length one.
Jonckheere I, Nackaerts K, Muys B, Coppin P (2005).
“Assessment of automatic gap fraction estimation of forests from digital hemispherical photography.”
Agricultural and Forest Meteorology, 132(1-2), 96–114.
doi:10.1016/j.agrformet.2005.06.003.
Ridler TW, Calvard S (1978).
“Picture thresholding using an iterative selection method.”
IEEE Transactions on Systems, Man, and Cybernetics, 8(8), 630–632.
doi:10.1109/tsmc.1978.4310039.
caim <- read_caim() thr_isodata(caim$Blue[])caim <- read_caim() thr_isodata(caim$Blue[])
Compute threshold values from background digital numbers (DN) using Equation 1 in Díaz and Lencinas (2018), a linear function whose slope can be weighted.
thr_mblt(dn, intercept, slope)thr_mblt(dn, intercept, slope)
dn |
numeric vector or terra::SpatRaster. Background digital number. Values must be normalized; if taken from JPEG, apply gamma back correction. |
intercept, slope
|
numeric vectors of length one. Linear coefficients. |
The model was derived from canopy targets (perforated, rigid, dark
surfaces) backlit under homogeneous illumination, photographed with a
Nikon Coolpix 5700 in JPEG mode. Images were gamma-back-corrected with
a default gamma of 2.2 (see invert_gamma_correction()). Results showed that the optimal
threshold is linearly related to the background DN (see Figures 1 and 7
in Díaz and Lencinas (2018)). This shifted the goal from
estimating an optimal threshold Song et al. (2014) to
estimating the background DN as if the canopy were absent, as proposed by
Lang et al. (2010).
To apply the weighting parameter (w) from Equation 1, supply slope as
.
Equation 1 was developed with 8-bit images. New coefficients should be
calibrated in the 0–255 domain, which is what thr_mblt() expects, even
though the dn argument must be normalized. This design choice harmonizes
behavior across the package.
An object of the same class and dimensions as dn.
Users are encouraged to acquire raw files (see read_caim_raw()).
Díaz GM, Lencinas JD (2018).
“Model-based local thresholding for canopy hemispherical photography.”
Canadian Journal of Forest Research, 48(10), 1204–1216.
doi:10.1139/cjfr-2018-0006.
Lang M, Kuusk A, Mõttus M, Rautiainen M, Nilson T (2010).
“Canopy gap fraction estimation from digital hemispherical images using sky radiance models and a linear conversion method.”
Agricultural and Forest Meteorology, 150(1), 20–29.
doi:10.1016/j.agrformet.2009.08.001.
Song GM, Doley D, Yates D, Chao K, Hsieh C (2014).
“Improving accuracy of canopy hemispherical photography by a constant threshold value derived from an unobscured overcast sky.”
Canadian Journal of Forest Research, 44(1), 17–27.
doi:10.1139/cjfr-2013-0082.
normalize_minmax(), invert_gamma_correction()
thr_mblt(invert_gamma_correction(125), -7.8, 0.95 * 0.5)thr_mblt(invert_gamma_correction(125), -7.8, 0.95 * 0.5)
Apply Rosin’s geometric corner detector for unimodal histograms Rosin (2001) to both sides of a bimodal canopy histogram as in Macfarlane’s two-corner approach (Macfarlane 2011). Optional slope-reduction as in Macfarlane (2011) is supported. Peak detection can use the multimode package or Macfarlane’s original windowed maxima.
thr_twocorner( x, sigma = 2, slope_reduction = FALSE, method = "multimode", diagnose = FALSE, adjust_par = TRUE )thr_twocorner( x, sigma = 2, slope_reduction = FALSE, method = "multimode", diagnose = FALSE, adjust_par = TRUE )
x |
numeric vector or a single-column |
sigma |
numeric vector of length one or |
slope_reduction |
logical vector of length one. If |
method |
character vector of length one. Peak detection strategy. One of
|
diagnose |
logical vector of length one. If |
adjust_par |
logical vector of length one. If |
Runs the following pipeline:
Build an 8-bit histogram of x after min–max normalization to
[0,255].
Smooth the histogram with a discrete Gaussian kernel of standard
deviation sigma (in DN), using reflective padding to mitigate edge
bias.
Detect the two mode peaks according to method:
method = "multimode": use multimode::locmodes(mod0 = 2).
method = "macfarlane": search peaks within fixed DN windows as in
Macfarlane (2011). Peak search is performed on the
unsmoothed histogram, as originally proposed.
Apply Rosin’s corner construction on each mode. If
slope_reduction = TRUE and the peak height exceeds the mean of the
smoothed histogram, the peak height is reduced to that mean before the
geometric construction (Macfarlane’s variant).
Derive thresholds:
where and are the lower and upper corners.
When diagnose = TRUE, a geometric construction like the one shown below is
sent to the active graphics device.
When diagnose = TRUE and adjust_par = TRUE, the function temporarily
adjusts margins to draw the geometric construction in a large square format
and restores the previous settings on exit. If adjust_par = FALSE, no
parameters are changed and the plot respects the current device/layout.
A list with:
Lower peak DN.
Lower corner DN (Rosin on the left mode).
Low threshold derived from lc and uc.
Mid threshold derived from lc and uc.
High threshold derived from lc and uc.
Upper corner DN (Rosin on the right mode).
Upper peak DN.
Macfarlane C (2011).
“Classification method of mixed pixels does not affect canopy metrics from digital images of forest overstorey.”
Agricultural and Forest Meteorology, 151(7), 833–840.
doi:10.1016/j.agrformet.2011.01.019.
Rosin PL (2001).
“Unimodal thresholding.”
Pattern Recognition, 34(11), 2083–2096.
ISSN 0031-3203.
doi:10.1016/s0031-3203(00)00136-9.
## Not run: caim <- conventional_lens_image() par(mfrow = c(1,2)) # Original Macfarlane thr <- thr_twocorner(caim$Blue[], sigma = NULL, slope_reduction = TRUE, method = "macfarlane", diagnose = T, adjust_par = FALSE) # Alternative with multimode thr2 <- thr_twocorner(caim$Blue[], sigma = 2, slope_reduction = FALSE, method = "multimode", diagnose = T, adjust_par = FALSE) data.frame(macfarlane = unlist(thr), alternative = unlist(thr2)) ## End(Not run)## Not run: caim <- conventional_lens_image() par(mfrow = c(1,2)) # Original Macfarlane thr <- thr_twocorner(caim$Blue[], sigma = NULL, slope_reduction = TRUE, method = "macfarlane", diagnose = T, adjust_par = FALSE) # Alternative with multimode thr2 <- thr_twocorner(caim$Blue[], sigma = 2, slope_reduction = FALSE, method = "multimode", diagnose = T, adjust_par = FALSE) data.frame(macfarlane = unlist(thr), alternative = unlist(thr2)) ## End(Not run)
Interpolate pixel values from irregularly spaced points using planar surfaces defined by Delaunay triangles.
triangulate(sky_points, r, col_id = "dn")triangulate(sky_points, r, col_id = "dn")
sky_points |
|
r |
numeric terra::SpatRaster with one layer. Image from which
|
col_id |
numeric or character vector of length one. The name or position
of the column in |
Each triangle defines a plane estimated from its three vertices, and interpolation is performed by evaluating the plane equation at each pixel position. This approach produces a continuous surface with sharp transitions at triangle borders.
Numeric terra::SpatRaster with one layer and the same geometry
as r.
The function internally uses terra::delaunay() to construct the
triangulated mesh.
## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) m <- !is.na(z) r <- caim$Blue bin <- binarize_by_region(r, ring_segmentation(z, 15), "thr_isodata") & select_sky_region(z, 0, 88) g <- skygrid_segmentation(z, a, 10) sky_points <- sample_sky_points(r, bin, g, dist_to_black = 3) plot(bin) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) sky_points <- extract_dn(r, sky_points) sky <- triangulate(sky_points, r, col_id = 3) plot(sky) ## End(Not run)## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) m <- !is.na(z) r <- caim$Blue bin <- binarize_by_region(r, ring_segmentation(z, 15), "thr_isodata") & select_sky_region(z, 0, 88) g <- skygrid_segmentation(z, a, 10) sky_points <- sample_sky_points(r, bin, g, dist_to_black = 3) plot(bin) points(sky_points$col, nrow(caim) - sky_points$row, col = 2, pch = 10) sky_points <- extract_dn(r, sky_points) sky <- triangulate(sky_points, r, col_id = 3) plot(sky) ## End(Not run)
Tune the parameters of sample_sky_points().
tune_sky_sampling( r, z, a, equalarea_seg, bin_list, n_cells_seq, dist_to_black_seq, w = 0.5, write_log_in = NULL, parallel = TRUE, cores = NULL, logical = TRUE, leave_free = 1 )tune_sky_sampling( r, z, a, equalarea_seg, bin_list, n_cells_seq, dist_to_black_seq, w = 0.5, write_log_in = NULL, parallel = TRUE, cores = NULL, logical = TRUE, leave_free = 1 )
r |
numeric terra::SpatRaster of one layer. Typically the blue band of a canopy photograph. Digital numbers should be linearly related to radiance. |
z |
terra::SpatRaster generated with |
a |
terra::SpatRaster generated with |
equalarea_seg |
terra::SpatRaster generated with |
bin_list |
list of logical terra::SpatRaster objects of one
layer. Each element is a binarization of |
n_cells_seq |
numeric vector. Sequence of integers to evaluate for
|
dist_to_black_seq |
numeric vector. Sequence of integers to evaluate for
|
w |
numeric vector of length one. Weight controlling the balance between coverage and accuracy (see Details). |
write_log_in |
Character path (without extension) used to generate two output files summarizing the tuning run. See Write one disk (optional). |
parallel |
logical vector of length one. If |
cores |
numeric vector of length one. Number of CPU cores to use when
|
logical |
logical vector of length one. Internally it is passed unchanged
to the argument |
leave_free |
numeric vector of length one. Number of CPU cores to leave
free when |
This function evaluates combinations of three discrete parameters for sampling
sky digital numbers () using
sample_sky_points():
the binarized image selected from bin_list, provided as argument
bin;
the parameter n_cells used to generate a segmentation with
equalarea_segmentation(), provided as argument seg;
the minimum allowed distance to black pixels in the binarized image,
provided as argument dist_to_black.
Two criteria are computed for each parameter combination:
accuracy, estimated from the local variability of
;
coverage, the fraction of equal-area cells containing at least one sampled sky point.
The computation of these criteria is described below, together with their normalization and the construction of the final comparison metric.
True sky regions tend to show smooth radiance gradients, whereas bright
canopy elements or mixed pixels introduce stronger local fluctuations.
Accuracy is therefore quantified with a robust local coefficient of
variation computed by extract_cv(). For each parameter combination, the
final accuracy value is the median of these local CV estimates across all
sampled sky points.
Coverage is computed with assess_sampling_uniformity(), i.e., the
fraction of cells in equalarea_seg that contain at least one sky point.
Because the magnitudes of accuracy and coverage may differ substantially
across the parameter grid, both components are rescaled to using
normalize_minmax(). This ensures that the relative contribution of each
component to the optimization reflects the balance specified by
rather than artifacts of scale.
After normalization, the Coverage-Accuracy Metric is defined as:
were is the nth parameter combination being tested.
Lower CAM values indicate parameter combinations that achieve both high hemispherical coverage and low local radiance variability at the sampled sky points. The function returns the combination that minimizes CAM.
List with the following numeric vectors of length one:
n_cellsOptimal value within n_cells_seq.
dist_to_blackOptimal value within dist_to_black_seq.
bin_list_indexIndex of the optimal binarization in bin_list.
accuracyMedian of local CV values (MAD / median) at the sampled sky points. Lower is better.
coverageFraction of reference cells with at least one sampled sky point. Higher is better.
camMinimum Coverage-Accuracy Metric (CAM) for the selected combination.
If write_log_in is not NULL, the function writes:
<write_log_in>_tune_log.txtA human-readable log containing timestamps, elapsed time (minutes), system information, the tested parameters, the selected combination, and its performance metrics. Use a named list as in the example to document how the binarized images were obtained.
<write_log_in>_tune_metrics.csvA tabular file listing all tested
parameter combinations (params) together with their associated
metrics. Load it with utils::read.csv2() and use which.min() to
retrieve the best parameters
If NULL, no files are written.
## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) m <- !is.na(z) r <- caim$Blue seg <- equalarea_segmentation(z, a, 30) bin_list <- lapply(c("thr_twocorner", "thr_isodata"), function(x) { binarize_by_region(r, seg, x) & select_sky_region(z, 0, 80) }) names(bin_list) <- paste( c("thr_twocorner", "thr_isodata"), "regional with 30 cells of equal area & select_sky_region(z, 0, 80)." ) equalarea_seg <- equalarea_segmentation(z, a, 10) params <- tune_sky_sampling(r, z, a, equalarea_seg, bin_list, n_cells_seq = seq(100, 500, 100), dist_to_black_seq = 1:10, w = 0.5, parallel = TRUE) sky_points <- sample_sky_points(r, bin_list[[params$bin_list_index]], equalarea_segmentation(z, a, params$n_cells), params$dist_to_black) display_caim(caim$Blue, sampling_points = sky_points) ## End(Not run)## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) m <- !is.na(z) r <- caim$Blue seg <- equalarea_segmentation(z, a, 30) bin_list <- lapply(c("thr_twocorner", "thr_isodata"), function(x) { binarize_by_region(r, seg, x) & select_sky_region(z, 0, 80) }) names(bin_list) <- paste( c("thr_twocorner", "thr_isodata"), "regional with 30 cells of equal area & select_sky_region(z, 0, 80)." ) equalarea_seg <- equalarea_segmentation(z, a, 10) params <- tune_sky_sampling(r, z, a, equalarea_seg, bin_list, n_cells_seq = seq(100, 500, 100), dist_to_black_seq = 1:10, w = 0.5, parallel = TRUE) sky_points <- sample_sky_points(r, bin_list[[params$bin_list_index]], equalarea_segmentation(z, a, params$n_cells), params$dist_to_black) display_caim(caim$Blue, sampling_points = sky_points) ## End(Not run)
Validate CIE sky models fitted with fit_cie_model() (or ootb_sky_cie())
using k-fold cross-validation on relative radiance.
validate_cie_model(model, k = 10)validate_cie_model(model, k = 10)
model |
list. Output of |
k |
numeric vector of length one. Number of folds. |
Validation uses k-fold cross-validation with k = 10 by default
(Kohavi 1995). For each fold, predictions are
compared against observed relative radiance and a simple linear regression
of predicted vs. observed is fitted, following
Piñeiro et al. (2008). Outliers are detected with a
median–MAD rule (see rem_outliers()) using a threshold of 3
and removed before fitting the regression.
A list with:
An object of class lm (see stats::lm()) for predicted vs. observed.
Numeric vector of predicted relative radiance used in lm.
Numeric vector of observed relative radiance used in lm.
Coefficient of determination ().
Root mean squared error (RMSE).
Median absolute error (MAE).
Logical vector marking outliers (MAD > 3) in the original sky-point set.
Numeric value. Mean squared deviation as in Gauch et al. (2003).
Gauch HG, Hwang JTG, Fick GW (2003).
“Model evaluation by comparison of model‐based predictions and measured values.”
Agronomy Journal, 95(6), 1442–1446.
ISSN 1435-0645.
doi:10.2134/agronj2003.1442.
Kohavi R (1995).
“A study of cross-validation and bootstrap for accuracy estimation and model selection.”
In Proceedings of the 14th International Joint Conference on Artificial Intelligence - Volume 2, IJCAI'95, 1137–1143.
ISBN 1558603638.
Piñeiro G, Perelman S, Guerschman JP, Paruelo JM (2008).
“How to evaluate models: Observed vs. predicted or predicted vs. observed?”
Ecological Modelling, 216(3-4), 316–322.
doi:10.1016/j.ecolmodel.2008.05.006.
## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) path <- system.file("external/sky_points.csv", package = "rcaiman") sky_points <- read.csv(path)[c("Y", "X")] names(sky_points) <- c("row", "col") rr <- extract_rr(caim$Blue, z, a, sky_points) set.seed(7) model <- fit_cie_model(rr, sun_angles = c(z = 49.5, a = 27.4), general_sky_type = "Clear", method = "CG") val <- validate_cie_model(model, k = 10) val$r_squared val$rmse ## End(Not run)## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) path <- system.file("external/sky_points.csv", package = "rcaiman") sky_points <- read.csv(path)[c("Y", "X")] names(sky_points) <- c("row", "col") rr <- extract_rr(caim$Blue, z, a, sky_points) set.seed(7) model <- fit_cie_model(rr, sun_angles = c(z = 49.5, a = 27.4), general_sky_type = "Clear", method = "CG") val <- validate_cie_model(model, k = 10) val$r_squared val$rmse ## End(Not run)
Wrapper around terra::writeRaster() that writes a canopy image as GeoTIFF
with 8- or 16-bit unsigned integers, setting CRS and extent.
write_caim(caim, path, bit_depth)write_caim(caim, path, bit_depth)
caim |
|
path |
character vector of length one. Destination file path (extension
|
bit_depth |
numeric vector of length one. Either |
Adds the .tif extension to path if missing. The CRS is set to EPSG:7589
and the extent to [0, ncol] × [0, nrow] in pixel units
(set_rcaiman_geometry()). Data are written as INT1U when bit_depth = 8
and INT2U when bit_depth = 16.
No return value. Called for side effects.
## Not run: caim <- read_caim() %>% normalize_minmax(0, 255) write_caim(caim * (2^8 - 1), file.path(tempdir(), "test_8bit"), 8) write_caim(caim * (2^16 - 1), file.path(tempdir(), "test_16bit"), 16) # Note: values are scaled by (2^bit_depth - 1) to avoid the maximum bin, # which read_caim() might turn NA. ## End(Not run)## Not run: caim <- read_caim() %>% normalize_minmax(0, 255) write_caim(caim * (2^8 - 1), file.path(tempdir(), "test_8bit"), 8) write_caim(caim * (2^16 - 1), file.path(tempdir(), "test_16bit"), 16) # Note: values are scaled by (2^bit_depth - 1) to avoid the maximum bin, # which read_caim() might turn NA. ## End(Not run)
Persist and restore the out-of-the-box CIE sky model, its diagnostics, and
related rasters/points. The writer produces a human-readable .txt model record
plus CSV and GeoPackage sidecar files. The reader reconstructs a list object
compatible with the out-of-the-box pipeline.
write_sky_cie(sky_cie, name) read_sky_cie(name, r, z, a, refit_allowed = FALSE)write_sky_cie(sky_cie, name) read_sky_cie(name, r, z, a, refit_allowed = FALSE)
sky_cie |
list. Object holding the fitted CIE model, diagnostics, and derived rasters, as produced by the out-of-the-box workflow. |
name |
character vector of length one. File base name without extension.
A path can be included, e.g., |
r |
numeric terra::SpatRaster of one layer. The canopy image
used in the out-of-the-box workflow (used by |
z |
terra::SpatRaster generated with |
a |
terra::SpatRaster generated with |
refit_allowed |
logical vector of length one. If |
Encoding is UTF-8. Decimal point is .. Unknown keys are ignored with a
warning. Missing required keys trigger an error. The model record begins with
format_version: which is checked for basic compatibility. It
reflects the on-disk model record format and differs from package
versioning.
When read_sky_cie() detects manual edits (moved sun disk or changed sky
points) and refit_allowed = TRUE, it re-fits the CIE model using the
current r, z, and a. The model record remains unchanged.
See Functions.
write_sky_cieNo return value. Writes four files to disk with the
prefix name (see below).
read_sky_cieReturns a list similar to the output of
ootb_sky_cie() and suitable as input to ootb_sky_above().
write_sky_cie
Plain text model record: <name>_model_record.txt
CSV with sky radiance samples: <name>_rr.csv
GeoPackage with sky sample points: <name>_sky_points.gpkg
GeoPackage with the sun disk location: <name>_sun_disk.gpkg
format_version:Semantic version of the model record.
sun_theta:Solar zenith (deg).
sun_phi:Solar azimuth (deg).
method_sun:Method used to optimize sun coordinates.
zenith_dn:Reference DN at zenith.
start_a:…start_e:
Initial CIE coefficients.
fit_a:…fit_e:
Fitted CIE coefficients.
method:Method used to fit CIE coefficients.
## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) # Read a previously written model path <- system.file("external/example.txt", package = "rcaiman") sky_cie <- read_sky_cie(gsub(".txt", "", path), r = caim$Blue, z = z, a = a, refit_allowed = TRUE) ## End(Not run)## Not run: caim <- read_caim() z <- zenith_image(ncol(caim), lens()) a <- azimuth_image(z) # Read a previously written model path <- system.file("external/example.txt", package = "rcaiman") sky_cie <- read_sky_cie(gsub(".txt", "", path), r = caim$Blue, z = z, a = a, refit_allowed = TRUE) ## End(Not run)
Bidirectional helpers to convert between angular coordinates on the
hemispherical image (zenith, azimuth) and raster
coordinates (row, col).
zenith_azimuth_from_row_col(z, a, row, col) row_col_from_zenith_azimuth(z, a, zenith, azimuth)zenith_azimuth_from_row_col(z, a, row, col) row_col_from_zenith_azimuth(z, a, zenith, azimuth)
z |
terra::SpatRaster generated with |
a |
terra::SpatRaster generated with |
row, col
|
numeric vectors. raster coodinates. Must have equal length. |
zenith, azimuth
|
numeric vectors. Angles in degrees. Must have equal length. |
zenith, azimuth → row, col.
A sparse set of valid sky points is sampled over the image and enriched with
their angular coordinates. Two local least-squares surfaces
(spatial::surf.ls, np = 6) are fitted to predict row and col as
smooth functions of (azimuth, zenith). Predictions are rounded to the
nearest integer index. Out-of-bounds indices are not produced under normal
conditions; clamp externally if needed.
row, col → zenith, azimuth.
Angles are obtained by direct lookup on z and a using
terra::cellFromRowCol. If any queried cell is NA (e.g., outside the
calibrated lens footprint), a synthetic z is reconstructed from the lens
model attached to z (attribute lens_coef), and a is rebuilt with
azimuth_image() using the stored orientation attribute in a. This yields
robust angle retrieval near borders.
See Functions
row_col_from_zenith_azimuthReturn image indices for given angles.
zenith_azimuth_from_row_colReturn angles in degrees for given image indices.
z <- zenith_image(1000, lens()) a <- azimuth_image(z) rc <- row_col_from_zenith_azimuth(z, a, zenith = c(30, 60), azimuth = c(90, 270)) rc ang <- zenith_azimuth_from_row_col(z, a, row = rc$row, col = rc$col) angz <- zenith_image(1000, lens()) a <- azimuth_image(z) rc <- row_col_from_zenith_azimuth(z, a, zenith = c(30, 60), azimuth = c(90, 270)) rc ang <- zenith_azimuth_from_row_col(z, a, row = rc$row, col = rc$col) ang
Build a single-layer image with zenith angle values, assuming upwards-looking hemispherical photography with the optical axis vertically aligned.
zenith_image(diameter, lens_coef)zenith_image(diameter, lens_coef)
diameter |
numeric vector of length one. Diameter in pixels expressed as an even integer. This places the zenith point between pixels. Snapping the zenith point between pixels does not affect accuracy because half-pixel is less than the uncertainty in localizing the circle within the picture. |
lens_coef |
numeric vector. Polynomial coefficients of the lens
projection function. See |
terra::SpatRaster with zenith angles in
degrees, showing a complete hemispherical view with the zenith at the
center. The object carries attributes lens_coef.
z <- zenith_image(600, lens("Nikon_FCE9")) plot(z)z <- zenith_image(600, lens("Nikon_FCE9")) plot(z)