| Title: | Estimate 3D Orientations from 2D Landmarks |
|---|---|
| Description: | Estimates possible 3D orientations of an object from the 2D image coordinates of two landmarks and an approximate camera-object elevation angle. Because a projected 2D angle can be compatible with multiple 3D pitch, yaw, and view-elevation angles, the package returns compatible sets of orientations rather than a single unconstrained estimate. |
| Authors: | João C T Menezes [aut, cre, cph] (ORCID: <https://orcid.org/0000-0003-4156-2934>) |
| Maintainer: | João C T Menezes <[email protected]> |
| License: | GPL (>= 3) |
| Version: | 1.0.1.9000 |
| Built: | 2026-07-03 07:49:48 UTC |
| Source: | https://github.com/jocateme/araponga |
Quick visual reminders of the angle conventions used by araponga.
conventions(type = c("pitch", "yaw", "view_elevation"))conventions(type = c("pitch", "yaw", "view_elevation"))
type |
Character vector specifying which convention plot(s) to draw.
Choices are |
No return value.
conventions() conventions("yaw")conventions() conventions("yaw")
deg2rad() takes angles in degrees and returns them converted in radians.
rad2deg() takes angles in radians and returns them converted in degrees.
deg2rad(deg) rad2deg(rad)deg2rad(deg) rad2deg(rad)
deg |
Numeric vector of angles in degrees. |
rad |
Numeric vector of angles in radians. |
A numeric vector of angles, in radians or degrees. Length matches deg or rad.
deg2rad(c(0, 90, 180)) rad2deg(c(0, pi/2, pi))deg2rad(c(0, 90, 180)) rad2deg(c(0, pi/2, pi))
Download the precomputed simulation dataset used by find.3d() and related functions.
download.simdata(overwrite = FALSE, quiet = FALSE)download.simdata(overwrite = FALSE, quiet = FALSE)
overwrite |
Logical scalar. If |
quiet |
Logical scalar. If |
The dataset is stored under tools::R_user_dir("araponga", "cache"). If the archive is already
available locally and overwrite = FALSE, the cached dataset is reused. See find.3d() for how
the dataset was constructed.
An invisible character scalar giving the normalized path to the extracted simulation dataset directory.
Using precomputed simulation data, recover the combinations of yaw, pitch, and view elevations
(at 1° resolution) that produce a given observed 2D pitch. find.pitch() and find.yaw() are
convenient wrappers for two common uses: finding 3D pitch and yaw, respectively.
find.3d( pitch2d, find = NULL, candidate_view_elevations = NULL, candidate_pitches = NULL, candidate_yaws = NULL, label_error, label_nsamp = 625, sim_download = FALSE ) find.yaw( pitch2d, candidate_view_elevations = NULL, candidate_pitches = NULL, candidate_yaws = NULL, paired = FALSE, label_error, label_nsamp = 625, sim_download = FALSE ) find.pitch( pitch2d, candidate_pitches = NULL, candidate_yaws = NULL, candidate_view_elevations = NULL, paired = FALSE, label_error, label_nsamp = 625, sim_download = FALSE )find.3d( pitch2d, find = NULL, candidate_view_elevations = NULL, candidate_pitches = NULL, candidate_yaws = NULL, label_error, label_nsamp = 625, sim_download = FALSE ) find.yaw( pitch2d, candidate_view_elevations = NULL, candidate_pitches = NULL, candidate_yaws = NULL, paired = FALSE, label_error, label_nsamp = 625, sim_download = FALSE ) find.pitch( pitch2d, candidate_pitches = NULL, candidate_yaws = NULL, candidate_view_elevations = NULL, paired = FALSE, label_error, label_nsamp = 625, sim_download = FALSE )
pitch2d |
Either a numeric scalar returned by |
find |
Character vector. Which angle(s) to return between "pitch", "yaw", "view_elevation", and
"pitch2d". Default ( |
candidate_view_elevations |
Optional numeric vector: known or candidate camera elevation angle(s)
relative to the object, in degrees, in the interval [-90, 90]. Convention: |
candidate_pitches |
Optional numeric vector: known or candidate (3D) pitch angle(s), in degrees,
in the interval [-90, 90]. Convention: |
candidate_yaws |
Optional numeric vector: known or candidate yaw angle(s), in degrees, in the
interval (-180, 180]. Convention: |
label_error |
Positive numeric scalar specifying the error (± pixels) used to perturb landmark
coordinates. Passed internally to |
label_nsamp |
Positive integer scalar specifying the approximate number of grid combinations to
evaluate. Passed internally to |
sim_download |
Logical scalar. If |
paired |
Logical scalar. If |
The lookup is based on a precomputed Blender simulation grid. A 3D pyramid was generated and rotated
by pitch, then yaw, then roll (equivalently to what rotate3d() does) for all 11,793,960
integer combinations of pitch (-90:90), yaw (-179:180), and roll (-90:90). For each combination
the base midpoint and tip were projected to 2D pixel coordinates with an orthographic camera.
The cached simulation table stores the resulting projected geometry and derived 2D pitch values;
find.3d() (and wrappers) then filter that table for combinations compatible with the requested 2D
pitch and any optional candidate constraints.
A data.frame with all or a subset of the following columns:
integer: pitch angles (degrees) compatible with the provided arguments.
integer: yaw angles (degrees) compatible with the provided arguments.
numeric: projected 2D pitch angles (degrees) compatible with the provided arguments.
integer: elevation angles (degrees) compatible with the provided arguments.
If no matches are found, the returned data.frame has zero rows.
For find.pitch(..., paired = FALSE) and find.yaw(..., paired = FALSE), an integer vector of
pitch or yaw angles compatible with the provided arguments.
pitch2d.from.xy(), pitch2d.w.error(), rotate3d(), download.simdata()
# test that dataset is downloaded before running examples sim_path <- file.path( tools::R_user_dir("araponga", "cache"), "sim_data_parquet" ) if(dir.exists(sim_path)){ # hypothetical pitch2d from coordinates p2d <- pitch2d.from.xy(10, 1, -12, 20) # pitches that project to `p2d` (± 2 pixel error) if seen from 30° (± 1° error) below find.pitch( p2d, candidate_view_elevations = -29:-31, label_error = 2, sim_download = FALSE ) # same returned values as from: find.3d( p2d, find = "pitch", candidate_view_elevations = -29:-31, label_error = 2, sim_download = FALSE ) # similar to above, now given yaw between 5° and 15° find.pitch( pitch2d = p2d, candidate_view_elevations = -29:-31, candidate_yaws = 5:15, label_error = 2, sim_download = FALSE ) # yaws that project to `p2d` (± 2 pixel error) if seen from 10° (± 2° error) below find.yaw( p2d, candidate_view_elevations = -8:-12, label_error = 2 ) } else { message( "Run download.simdata() first to enable this example." ) }# test that dataset is downloaded before running examples sim_path <- file.path( tools::R_user_dir("araponga", "cache"), "sim_data_parquet" ) if(dir.exists(sim_path)){ # hypothetical pitch2d from coordinates p2d <- pitch2d.from.xy(10, 1, -12, 20) # pitches that project to `p2d` (± 2 pixel error) if seen from 30° (± 1° error) below find.pitch( p2d, candidate_view_elevations = -29:-31, label_error = 2, sim_download = FALSE ) # same returned values as from: find.3d( p2d, find = "pitch", candidate_view_elevations = -29:-31, label_error = 2, sim_download = FALSE ) # similar to above, now given yaw between 5° and 15° find.pitch( pitch2d = p2d, candidate_view_elevations = -29:-31, candidate_yaws = 5:15, label_error = 2, sim_download = FALSE ) # yaws that project to `p2d` (± 2 pixel error) if seen from 10° (± 2° error) below find.yaw( p2d, candidate_view_elevations = -8:-12, label_error = 2 ) } else { message( "Run download.simdata() first to enable this example." ) }
Compute the 2D pitch produced by 3D orientations (pitch, yaw) when viewed from a specified elevation.
pitch2d.from.3d(pitch, yaw, view_elevation, plot = FALSE)pitch2d.from.3d(pitch, yaw, view_elevation, plot = FALSE)
pitch |
Numeric scalar: vertical angle relative to horizontal plane, in degrees, in the
interval [-90, 90]. Convention: |
yaw |
Numeric scalar: horizontal angle around the vertical axis, in degrees, in the
interval (-180, 180]. Convention: |
view_elevation |
Numeric scalar: camera elevation angle relative to the object, in degrees, in
the interval [-90, 90]. Convention: |
plot |
Logical scalar. |
The function constructs the full rotation matrix using
rotate3d(pitch, yaw, view_elevation) and computes the projected 2D pitch as
Numeric scalar: 2D pitch angle, in degrees, in the interval (-180°, 180°].
Convention: 0 points right, 90 points up, -90 points down, 180 points left.
# object pointed up 15 degrees pitch2d.from.3d(15, 0, 0, plot = TRUE) # object pointed up 15 and 30 degrees toward camera pitch2d.from.3d(15, -30, 0, plot = TRUE) # object pointed up 15 degrees, looked at from 30 degrees below pitch2d.from.3d(15, 0, -30, plot = TRUE)# object pointed up 15 degrees pitch2d.from.3d(15, 0, 0, plot = TRUE) # object pointed up 15 and 30 degrees toward camera pitch2d.from.3d(15, -30, 0, plot = TRUE) # object pointed up 15 degrees, looked at from 30 degrees below pitch2d.from.3d(15, 0, -30, plot = TRUE)
Compute the 2D pitch angle defined by two landmarks: a tip point and a base point.
pitch2d.from.xy(x_tip, y_tip, x_base, y_base, plot = FALSE)pitch2d.from.xy(x_tip, y_tip, x_base, y_base, plot = FALSE)
x_tip, x_base
|
Numeric scalars or vectors: image x-coordinate(s) of tip and base of object, with x increasing right. |
y_tip, y_base
|
Numeric scalars or vectors: image y-coordinate(s) of the tip and base of objects, with y increasing up. |
plot |
Logical scalar. |
The calculation uses the two-landmark vector
and returns
Numeric vector of 2D pitch angles, in degrees, in the interval (-180°, 180°].
Convention: 0 points right, 90 points up, -90 points down, 180 points left.
Scalar outputs also include an "xy" attribute containing the input
coordinates, which makes it directly compatible with find.3d().
pitch2d.w.error(), pitch2d.from.3d()
# scalar examples (plots) pitch2d.from.xy(1, 0, 0, 0, plot = TRUE) # pointed right -> 0° pitch2d.from.xy(-1, 0, 0, 0, plot = TRUE) # pointed left -> 180° pitch2d.from.xy(0, 1, 0, 0, plot = TRUE) # pointed up -> 90° pitch2d.from.xy(0, -1, 0, 0, plot = TRUE) # pointed down -> -90° # vectorised usage (no plot) x_tips <- c(1, 0, -1) y_tips <- c(0, 1, 0) x_bases <- c(0, 0, 0) y_bases <- c(0, 0, 0) pitch2d.from.xy(x_tips, y_tips, x_bases, y_bases)# scalar examples (plots) pitch2d.from.xy(1, 0, 0, 0, plot = TRUE) # pointed right -> 0° pitch2d.from.xy(-1, 0, 0, 0, plot = TRUE) # pointed left -> 180° pitch2d.from.xy(0, 1, 0, 0, plot = TRUE) # pointed up -> 90° pitch2d.from.xy(0, -1, 0, 0, plot = TRUE) # pointed down -> -90° # vectorised usage (no plot) x_tips <- c(1, 0, -1) y_tips <- c(0, 1, 0) x_bases <- c(0, 0, 0) y_bases <- c(0, 0, 0) pitch2d.from.xy(x_tips, y_tips, x_bases, y_bases)
pitch2d.w.error() simulates the effect of landmark-labeling uncertainty on a 2D pitch
value returned by pitch2d.from.xy().
pitch2d.w.error( pitch2d, label_error, label_nsamp = 625, add_boundaries = FALSE )pitch2d.w.error( pitch2d, label_error, label_nsamp = 625, add_boundaries = FALSE )
pitch2d |
Numeric scalar returned by |
label_error |
Positive numeric scalar specifying the error (± pixels) used to perturb each landmark coordinate. |
label_nsamp |
Positive integer scalar specifying the approximate number of grid combinations to evaluate. Default 625 uses a 5-point grid for each of the four coordinates, for 5^4 = 625 total combinations. |
add_boundaries |
Logical scalar (default |
Numeric vector of unique simulated 2D pitch angles, in degrees, in the interval (-180, 180].
# A pitch value computed from landmark coordinates p2d <- pitch2d.from.xy(x_tip = 100, y_tip = 80, x_base = 110, y_base = 120) # Simulate the effect of ±1 pixel labeling uncertainty pitch2d.w.error(p2d, label_error = 1) #' # Simulate the effect of ±5 pixel labeling uncertainty pitch2d.w.error(p2d, label_error = 5)# A pitch value computed from landmark coordinates p2d <- pitch2d.from.xy(x_tip = 100, y_tip = 80, x_base = 110, y_base = 120) # Simulate the effect of ±1 pixel labeling uncertainty pitch2d.w.error(p2d, label_error = 1) #' # Simulate the effect of ±5 pixel labeling uncertainty pitch2d.w.error(p2d, label_error = 5)
Plotting helper for quick visual inspection of angles, in particular pitches, yaws, and view elevations.
## S3 method for class 'angles' plot( angles = NULL, type = NULL, facing = c("right", "left"), col = NULL, main = NULL, labels = TRUE, add = FALSE )## S3 method for class 'angles' plot( angles = NULL, type = NULL, facing = c("right", "left"), col = NULL, main = NULL, labels = TRUE, add = FALSE )
angles |
Set(s) of angles to be plotted, in degrees in the range (-180, 180]. If a numeric vector,
one plot is generated. If a |
type |
When |
facing |
Character choice between |
col |
Optional vector of colors to draw angle segments in each plot. Recycled to match
|
main |
Optional character vector: titles for each plot. Recycled to match |
labels |
Logical scalar (default |
add |
Logical scalar (default |
No return value.
# calls with a vector ## pitch between 60 and 70 plot.angles(60:70, "pitch") ## view_elevation between -30 and -40 plot.angles(-30:-40, "view_elevation") # call with named list list <- list(pitch = 10:30, yaw = -100:-45, view_elevation = -10:-15) plot.angles(list) # call with output from find.3d # test that dataset is downloaded before running example sim_path <- file.path( tools::R_user_dir("araponga", "cache"), "sim_data_parquet" ) if(dir.exists(sim_path)){ ## pitches and yaws that project to 10° (± 1° error) if seen from 15° below df <- find.3d( 9:11, find = c("pitch", "yaw"), candidate_view_elevations = -15, sim_download = FALSE ) plot.angles(df) } else { message( "Run download.simdata() first to enable this example." ) }# calls with a vector ## pitch between 60 and 70 plot.angles(60:70, "pitch") ## view_elevation between -30 and -40 plot.angles(-30:-40, "view_elevation") # call with named list list <- list(pitch = 10:30, yaw = -100:-45, view_elevation = -10:-15) plot.angles(list) # call with output from find.3d # test that dataset is downloaded before running example sim_path <- file.path( tools::R_user_dir("araponga", "cache"), "sim_data_parquet" ) if(dir.exists(sim_path)){ ## pitches and yaws that project to 10° (± 1° error) if seen from 15° below df <- find.3d( 9:11, find = c("pitch", "yaw"), candidate_view_elevations = -15, sim_download = FALSE ) plot.angles(df) } else { message( "Run download.simdata() first to enable this example." ) }
Create a 3×3 rotation matrix corresponding to sequential Euler rotations about Z, then Y, then X (i.e., ZYX or pitch–yaw–roll sequence).
rotate3d(pitch = 0, yaw = 0, roll = 0) Rz(pitch = 0) Ry(yaw = 0) Rx(roll = 0)rotate3d(pitch = 0, yaw = 0, roll = 0) Rz(pitch = 0) Ry(yaw = 0) Rx(roll = 0)
pitch |
Numeric scalar: angle in degrees for rotation about the Z axis. Defaults to |
yaw |
Numeric scalar: angle in degrees for rotation about the Y axis. Defaults to |
roll |
Numeric scalar: angle in degrees for rotation about the X axis. Defaults to |
A numeric 3×3 matrix (class matrix).
# rotate 45 about z, then 30 about y, then -80 about x rotate3d(45, 30, -80) # rotate 30 degrees about x Rx(30)# rotate 45 about z, then 30 about y, then -80 about x rotate3d(45, 30, -80) # rotate 30 degrees about x Rx(30)
Given a set of yaw angles (or any set of angles that wrap around ± 180), summarize.yaws() returns
the endpoints and width of the smallest continuous angular interval that contains all supplied angles.
summarize.yaws(yaws, plot = FALSE, tie_action = c("all", "error"))summarize.yaws(yaws, plot = FALSE, tie_action = c("all", "error"))
yaws |
Numeric vector of yaw angles, in degrees, in the interval (-180, 180]. |
plot |
Logical scalar. |
tie_action |
Choice between |
A named list with components:
numeric scalar: interval start (clockwise-most endpoint), in degrees.
numeric scalar: interval end (counterclockwise-most endpoint), in degrees.
numeric scalar: angular width of the shortest enclosing arc, in degrees.
logical: TRUE if the interval crosses the ±180 boundary.
list containing the output (from, to, width, wrap) for all alternative intervals, if any
# simple non-wrapping interval summarize.yaws(-60:60, plot = TRUE) # a wrapping interval (points around ±180) summarize.yaws(c(-175:-170, 171:179), plot = TRUE) # singleton (no plotting) summarize.yaws(30) # more than one interval possible (returns warning) summarize.yaws(c(-90, 90)) # output from find.yaw() # test that dataset is downloaded before running example sim_path <- file.path( tools::R_user_dir("araponga", "cache"), "sim_data_parquet" ) if(dir.exists(sim_path)){ yaws <- find.yaw( pitch2d = 14:16, candidate_view_elevations = -45, candidate_pitches = 10:20, sim_download = FALSE ) summarize.yaws(yaws, plot = TRUE) } else { message( "Run download.simdata() first to enable this example." ) }# simple non-wrapping interval summarize.yaws(-60:60, plot = TRUE) # a wrapping interval (points around ±180) summarize.yaws(c(-175:-170, 171:179), plot = TRUE) # singleton (no plotting) summarize.yaws(30) # more than one interval possible (returns warning) summarize.yaws(c(-90, 90)) # output from find.yaw() # test that dataset is downloaded before running example sim_path <- file.path( tools::R_user_dir("araponga", "cache"), "sim_data_parquet" ) if(dir.exists(sim_path)){ yaws <- find.yaw( pitch2d = 14:16, candidate_view_elevations = -45, candidate_pitches = 10:20, sim_download = FALSE ) summarize.yaws(yaws, plot = TRUE) } else { message( "Run download.simdata() first to enable this example." ) }
Trim two candidate yaw sets to angles that have at least one compatible partner in the other set, given minimum and maximum directional separation constraints.
trim.yaws(ccw_yaws, cw_yaws, min_sep, max_sep, plot = FALSE)trim.yaws(ccw_yaws, cw_yaws, min_sep, max_sep, plot = FALSE)
ccw_yaws |
Numeric vector: candidate yaw angles known/expected to be
counterclockwise of |
cw_yaws |
Numeric vector: candidate yaw angles known/expected to be clockwise of
|
min_sep |
Non-negative numeric scalar: the minimum angular separation (degrees) allowed between a value in one set and a partner in the other set. |
max_sep |
Non-negative numeric scalar: the maximum angular separation (degrees) allowed between a value in one set and a partner in the other set. |
plot |
Logical scalar. |
Given two sets of candidate yaw angles, trim.yaws() removes elements from each set that
do not have at least one partner in the other set that is at least min_sep and up to
max_sep clockwise (for ccw_yaws set) or counterclockwise (for cw_yaws) from it. In
other words, each retained ccw_yaws angle will be >= min_sep and <= max_sep
counterclockwise of at least one cw_yaws angle. Analogously, each retained cw_yaws angle
will be >= min_sep and <= max_sep clockwise of at least one ccw_yaws angle.
A list with elements:
numeric: subset of ccw_yaws that have at least one matching cw_yaws.
numeric: subset of cw_yaws that have at least one matching ccw_yaws.
# hypothetical candidate yaw sets: a <- 10:80 b <- 30:90 # we know `a` to be CCW of `b` by up to 180° trim.yaws(a, b, 0, 180, plot = TRUE) # we know `a` to be CCW of `b` by 30 to 45° trim.yaws(a, b, 30, 45, plot = TRUE) # if no mutual partners exist, both returned vectors become empty trim.yaws(ccw_yaws = c(-10), cw_yaws = c(130), min_sep = 0, max_sep = 180)# hypothetical candidate yaw sets: a <- 10:80 b <- 30:90 # we know `a` to be CCW of `b` by up to 180° trim.yaws(a, b, 0, 180, plot = TRUE) # we know `a` to be CCW of `b` by 30 to 45° trim.yaws(a, b, 30, 45, plot = TRUE) # if no mutual partners exist, both returned vectors become empty trim.yaws(ccw_yaws = c(-10), cw_yaws = c(130), min_sep = 0, max_sep = 180)