knitr::opts_chunk$set( collapse = TRUE, comment = "#>" )
The imuf package performs sensor fusion for an inertial measurement unit (IMU) that has a 3-axis accelerometer and a 3-axis gyroscope.
Specifically, compUpdate()
uses complementary filtering to estimate the sensor's final orientation, given its initial orientation, sensor readings of accelerations and angular velocities at a time point, time duration between data samples, and a gain factor (between 0 and 1) specifying the weighting of the accelerometer measurements.
This vignette describes how one may use the imuf package to analyze a real world dataset of IMU measurements.
library(imuf) library(purrr) library(ggplot2)
The walking_shin_1
dataset contains 31,946 rows of sensor readings. Each reading consists of 3 accelerations (m/s^2) and 3 angular velocities (rad/sec) measurements for north (x), east (y), and down (z) directions. The data sampling rate is 50 Hz, which translates to a time duration of 0.02 second between readings.
head(walking_shin_1)
To prepare for subsequent analyses, we first convert the dataframe to a list:
lst_ned_in <- as.list(as.data.frame(t(walking_shin_1))) %>% unname head(lst_ned_in, 2)
We will now look at how to update our sensor orientation given the IMU measurements. We will do that in 3 steps:
We first wrap compUpdate()
in a helper function:
myCompUpdate <- function(initQ, accgyr) { acc <- accgyr[1:3] gyr <- accgyr[4:6] dt <- 1/50 gain <- 0.1 orientation <- compUpdate(acc, gyr, dt, initQ, gain) orientation }
Next, we use the helper function to process the first two sensor readings in our dataset. For the processing of the first reading, we simply assume that the sensor's initial orientation is aligned with the world frame. However, for the procesing of the second reading, we take the output of the processing of the first reading as the initial orientation.
(q1 <- myCompUpdate(c(1, 0, 0, 0), lst_ned_in[[1]])) (q2 <- myCompUpdate(q1, lst_ned_in[[2]]))
Now we will process the entire list of IMU readings. To do that we take advantage of purrr::accumulate()
which automatically takes the output of the current iteration as the input to the next iteration:
orientations <- purrr::accumulate(lst_ned_in, myCompUpdate, .init = c(1, 0, 0, 0)) head(orientations, 5)
Note that the length of the output list is one more than that of the input list, with the extra element being the initial quaternion of c(1, 0, 0, 0)
.
The result of the previous step is a list of sensor orientations expressed as unit 4-vector rotation quaternions. We can use these rotation quaternions to transform any vector in the sensor's body frame into the world frame.
Since the walking_shin_1
dataset comes a sensor strapped onto the shin of a subject while she walked for 10 minutes, as an illustration we will use the sensor orientations to study the turns taken by the subject during her journey.
We will do that in 3 steps:
rotV()
to transform a vector from body frame to world frameWe can transform any vector from the body frame to the world frame by rotating the vector by the orientation of the sensor. rotV()
performs such a rotation. For example, rotating a vector pointing in the east-direction (c(0, 1, 0)
) about the north-direction by 90 degrees results in a vector pointing in the down-direction (c(0, 0, 1)
):
q <- c(cos(pi/4), sin(pi/4), 0, 0) vin <- c(0, 1, 0) rotV(q, vin)
Next, we write a function to compute the turn angle from the rotated vector:
getTurnAngle <- function(quat) { # a function to rotate c(1, 0, 0) by quat # and then compute the angle between (1, 0, 0) and the rotated vector # projected onto the n-e plane and # this construct is to detect turns rotVec <- rotV(quat, c(1, 0, 0)) theta <- atan2(rotVec[2], rotVec[1]) * 180 / pi theta }
Lastly, we compute all the turn angles using purrr::map()
:
turnAngles <- orientations %>% purrr::map(getTurnAngle) %>% unlist() head(turnAngles)
Let's take a look at the results:
# # create a vector of time stamps in minutes # note that sampling frequency is 50 Hz x <- 1:length(turnAngles) / 50 / 60 # ggplot2::ggplot(mapping = aes(x = x, y = turnAngles)) + ggplot2::geom_line()
There are some sharp jumps in the turn angles. And the reason for that is atan2()
restricts the angles to -180 and +180. So an angle of 181 becomes -179 breaking continuity. We can use a function to remove those artificial jumps and maintain continuity:
# # a function to remove artificial jumps in turn angles rmJumps <- function(theta) { firstDiffs <- diff(theta) bigDiffIdx <- which(abs(firstDiffs) > 100) # # fix #1 theta[(bigDiffIdx[1]+1):bigDiffIdx[2]] <- theta[(bigDiffIdx[1]+1):bigDiffIdx[2]] + 360 # # fix #2 theta[(bigDiffIdx[3]+1):bigDiffIdx[4]] <- theta[(bigDiffIdx[3]+1):bigDiffIdx[4]] + 360 # # fix #3 theta[(bigDiffIdx[4]+1):length(theta)] <- theta[(bigDiffIdx[4]+1):length(theta)] + 2*360 theta } # # remove artificial jumps turnAnglesNoJumps <- rmJumps(turnAngles) # # plot it ggplot2::ggplot(mapping = aes(x = x, y = turnAnglesNoJumps)) + ggplot2::geom_line()
There remains some jumps in turn angles. But these jumps are not artificial. They reflect the actual behaviors of the subject during her journey. For example, at 5 minute mark, the data suggests she made a 180 degree turn. And this can indeed be confirmed by the video.
# # zero in on +/- 10 sec around 5 minute mark idx_5min <- c(14800:15750) x_5min <- x[idx_5min] turn_5min <- turnAnglesNoJumps[idx_5min] # # plot it ggplot2::ggplot(mapping = aes(x = x_5min, y = turn_5min)) + ggplot2::geom_line()
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.