---
title: "imuf"
output: rmarkdown::html_vignette
description:
Learn how to use `compUpdate()` and `rotV()` functions to analyze a dataset of
accelerometer and gyroscope measurements from real world activities.
vignette: >
%\VignetteIndexEntry{imuf}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
---
```{r, include = FALSE}
knitr::opts_chunk$set(
collapse = TRUE,
comment = "#>"
)
```
## Introduction
The imuf package performs sensor fusion for an inertial measuremnt unit (IMU) that has a 3-axis accelerometer and a 3-axis gyroscope.
Specifically, `compUpdate()` uses [complementary filtering](https://stanford.edu/class/ee267/notes/ee267_notes_imu.pdf) 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.
```{r setup, message = FALSE}
library(imuf)
library(purrr)
library(ggplot2)
```
## Data
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.
```{r}
head(walking_shin_1)
```
To prepare for subsequent analyses, we first convert the dataframe to a list:
```{r}
lst_ned_in <- as.list(as.data.frame(t(walking_shin_1))) %>% unname
head(lst_ned_in, 2)
```
## Orientation update
We will now look at how to update our sensor orientation given the IMU measurements. We will do that in 3 steps:
* Create a helper function
* Update sensor orientation for one IMU reading
* Update sensor orientation for a list of IMU readings
### Helper function
We first wrap `compUpdate()` in a helper function:
```{r}
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
}
```
### Orientation update for one IMU reading
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 inital orientation.
```{r}
(q1 <- myCompUpdate(c(1, 0, 0, 0), lst_ned_in[[1]]))
(q2 <- myCompUpdate(q1, lst_ned_in[[2]]))
```
### Orientation update for multiple IMU readings
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:
```{r}
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)`.
## Application of orientations
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:
* Use `rotV()` to transform a vector from body frame to world frame
* Create a function to calculate the turn angle
* Compute the turn angles at every time point
### Vector transformation
We 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)`):
```{r}
q <- c(cos(pi/4), sin(pi/4), 0, 0)
vin <- c(0, 1, 0)
rotV(q, vin)
```
### Turn angle function
Next, we write a function to compute the turn angle from the rotated vector:
```{r}
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
}
```
### Turn angles for all time points
Lastly, we compute all the turn angles using `purrr::map()`:
```{r}
turnAngles <- orientations %>% purrr::map(getTurnAngle) %>% unlist()
head(turnAngles)
```
### Analyses of turn angles
Let's take a look at the results:
```{r}
#
# 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:
```{r}
#
# 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](http://wifo5-14.informatik.uni-mannheim.de/sensor/dataset/realworld2016/proband1/videos/video_walking.webm).
```{r}
#
# 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()
```