Smooths a trajectory while undoing the inward "corner-cutting" bias that a plain moving average introduces on curved paths.
Arguments
- data
A data frame of 2 or 3 numeric coordinate columns. The curvature math is Cartesian-specific (cross products, Euclidean norms, circumradius), so coordinates must be Cartesian. For a whole aniframe use
filter_across(), which selects the spatial columns fromvariables_whereand rejects non-Cartesian frames.- window_width_ma
Integer width of the moving-average kernel (must be odd; even values are rounded up). Larger = more smoothing. Default
11.- window_width_cc
Integer width of the curvature-correction kernel (must be odd). Larger = smoother correction but uses curvature info from further away. Default
7.- kernel
Kernel shape for both stages. One of
"hanning"(default; raised cosine) or"uniform"(boxcar).- boundary
Edge-handling strategy. Currently only
"padding"(repeat the first and last point so output length equals input length).- cc_mode
If
FALSE, returns just the moving-average result without curvature correction. Useful for comparison.- na_action
Method used to fill
NAvalues before filtering, so the filter sees a complete series. One of"linear"(default),"spline","stine","locf","value", or"error"to abort whenNAs are present. Filling is internal: whether the filled values reach the output is controlled bykeep_na.- keep_na
Logical. If
TRUE(default), positions that wereNAin the input areNAin the output — gaps stay gaps. IfFALSE, the values used to fill those gaps are kept, so the output has fewerNAs than the input and genuinely-missing stretches come back as interpolated estimates.- ...
Additional arguments passed to
replace_na_with()(e.g.value,min_gap,max_gap).
Details
A plain moving average pulls each point toward the chord between its neighbours, which lies inside the curve — so smoothed circles shrink inward. CCMA (Steinecker & Wuensche, 2023) estimates how much shrinkage the moving average caused at each point — from the local curvature and the kernel — and pushes the result back outward by exactly that amount.
The algorithm has two stages:
Moving average of the spatial coordinates with a kernel of width
window_width_ma.Curvature correction: at each output position, sum a kernel of width
window_width_ccof curvature-derived shifts and apply them outward in the curve's plane.
Because curvature is intrinsically multi-dimensional, this filter
operates on all spatial coordinates jointly (unlike the per-column
filters dispatched through filter_across()). It is most useful
for smoothing curved 2D or 3D trajectories where a plain moving
average visibly cuts corners; for general-purpose time-series
smoothing reach for filter_gaussian() or filter_sgolay().
Input shape
This is a column-level function: it takes a data frame of coordinate
columns and returns one of the same shape. The aniframe tier is
filter_across(), which applies it within the frame's existing grouping
so each individual / track / keypoint is smoothed as its own trajectory.
filter_across(af, "ccma") # a whole aniframe
data |> mutate(filter_ccma(pick(all_of(c("x", "y"))))) # the columnsdplyr::pick() supplies the columns and the result is spliced back over
them.
CCMA is multivariate — each output coordinate depends on all of them —
so it cannot be used with dplyr::across(), which passes one column at
a time.
References
Steinecker, T. & Wuensche, H.-J. (2023). A Simple and Model-Free Path Filtering Algorithm for Smoothing and Accuracy. 2023 IEEE Intelligent Vehicles Symposium (IV).
Reference Python implementation: https://github.com/UniBwTAS/ccma
Examples
# The column tier: a data frame of coordinates
coords <- data.frame(x = sin(seq(0, 6, length.out = 40)), y = seq(0, 6, length.out = 40))
head(filter_ccma(coords, window_width_ma = 11, window_width_cc = 7))
#> x y
#> 1 0.1335266 0.1323343
#> 2 0.2193066 0.2202318
#> 3 0.3248390 0.3313703
#> 4 0.4431924 0.4611623
#> 5 0.5652973 0.6035949
#> 6 0.6818460 0.7529515
# The aniframe tier
af <- anicore::example_aniframe(n_obs = 60, n_individuals = 1, n_keypoints = 1)
filter_across(af, "ccma", window_width_ma = 11, window_width_cc = 7)
#> # Individuals: 1
#> # Keypoints: centroid
#> # Sessions: 1
#> # Trials: 1
#> individual keypoint session trial time x y confidence
#> <int> <fct> <int> <int> <int> <dbl> <dbl> <dbl>
#> 1 1 centroid 1 1 1 -1.10 0.229 0.450
#> 2 1 centroid 1 1 2 -0.916 -0.0276 0.830
#> 3 1 centroid 1 1 3 -0.726 -0.257 0.438
#> 4 1 centroid 1 1 4 -0.530 -0.413 0.761
#> 5 1 centroid 1 1 5 -0.313 -0.444 0.896
#> 6 1 centroid 1 1 6 -0.159 -0.296 0.873
#> 7 1 centroid 1 1 7 -0.186 -0.150 0.687
#> 8 1 centroid 1 1 8 -0.211 -0.0153 0.605
#> 9 1 centroid 1 1 9 -0.182 0.124 0.944
#> 10 1 centroid 1 1 10 -0.0986 0.260 0.653
#> # ℹ 50 more rows