3 Guides

3.1 Importing trial data

All sensor data from an .exp file can be imported with a single call to ImportMotionData() from the package motionImport, which automatically scrapes the sensor names from the file header, and assembles the position and rotation data into a nested list.

The function requires a path leading to the .exp file, a numeric vector timestamps giving the start and end times of the recording (in this case, .5 seconds before stimulus onset to 1.5 seconds after), and an optional argument na.symbol (defaulting to 0) indicating the symbol used by the motion monitor output to denote missing values, which will be replaced with NA in the resulting R object.

An example:

filename <- 'data/MDim1_s1_b1_0000.exp'
data <- ImportMotionData(filename, timestamps = c(-.5, 1.5), na.symbol = 0)

The file data/MDim1_s1_b1_0000.exp contains data from a single grasping trial recorded at 60 Hz over a duration of 2 seconds, for a total of 120 samples. Position and rotation data (encoded by a position vector and a unit quaternion, respectively) are recorded at each sample from four sensors (thumb, index, middle finger, and back of hand) on both the right and left hands. The resulting object data is a nested list with the following structure:

data
+-- RtmbS2
|   +-- Timestamps
|   +-- Position
|   +-- Rotation
+-- RindexS3
|   +-- Timestamps
|   +-- Position
|   +-- Rotation
+-- RmiddleS4
|   +-- Timestamps
|   +-- Position
|   +-- Rotation
.
.
. etc

where timestamps is a numeric vector of length 120 with timestamps for each sample, Position is a 120 x 3 matrix of Cartesian coordinates denoting the position of the sensor, and Rotation is a 120 x 4 matrix of unit quaternions, denoting it’s orientation.

3.2 Seacurve trajectories

The next step is to represent the position and rotation of the sensor as a single trajectory in SE(3), represented by a sequence of dual quaternions. The object is represented as an object of S3 class Seacurve, though this is mostly just for type checking, and no S3 methods are currently implementd for the class. Creating a Seacurve object requires a time x 3 matrix giving the Cartesian coordinates of the object at each time step, and a time x 4 matrix of unit quaternions. Optionally, the user can also provide a vector of time stamps.

We can create a Seacurve object for the data from the right thumb as follows:

x  <- sc.posrot2dq(v = data$RtmbS2$Position,
                   q = data$RtmbS2$Rotation,
                   timestamps = data$RtmbS2$Timestamps)

We can easily recover the original position and rotation data using sc.dq2position() and sc.dq2rotation()

head(sc.dq2position(x))
##            Time        x       y        z
## [1,] -0.5000000 0.019186 0.09589 0.041145
## [2,] -0.4831933 0.019242 0.09589 0.041145
## [3,] -0.4663866       NA      NA       NA
## [4,] -0.4495798 0.019242 0.09589 0.041145
## [5,] -0.4327731       NA      NA       NA
## [6,] -0.4159664 0.019242 0.09589 0.041145
head(sc.dq2rotation(x))
##            Time        q0        q1         q2        q3
## [1,] -0.5000000 0.6652548 0.4429709 -0.3889559 0.4581769
## [2,] -0.4831933 0.6651109 0.4431029 -0.3889339 0.4582769
## [3,] -0.4663866        NA        NA         NA        NA
## [4,] -0.4495798 0.6651000 0.4430970 -0.3889980 0.4582440
## [5,] -0.4327731        NA        NA         NA        NA
## [6,] -0.4159664 0.6651000 0.4430970 -0.3889980 0.4582440

Seacurve also implements some basic plotting functions. We can view position and rotation data separately using

sc.plot(x, 'Position')
## Warning: Removed 24 rows containing missing values (geom_point).

sc.plot(x, 'Rotation')
## Warning: Removed 32 rows containing missing values (geom_point).

or the full dual quaternion data

sc.plot(x, 'DQ')
## Warning: Removed 64 rows containing missing values (geom_point).

3.3 Orienting the trajectory

Notice the discontinuities in the rotation and dual quaternion plots. Because antipodal quaternions (and dual quaternions) encode identical transformations, we must choose a consistent orientation for the data in order to guarantee smooth trajectories. The sc.orient() method accomplishes this by orienting the initial observation to have positive first coordinate, and then sequentially reorienting each observation to maximize smoothness. This is easy to do:

x <- sc.orient(x)

After which we can plot the rotation component again and see that the discontinuities have been removed:

sc.plot(x, 'Rotation')
## Warning: Removed 32 rows containing missing values (geom_point).

Note that it is essential to orient the trajectory BEFORE performing subsequent preprocessing, as discontinuities in the trajectory can severely impact procedures like interpolation.

3.4 Outlier removal

Because the sensor recordings are generally highly accurate (relative to the movement), trajectories don’t often have “outliers” of the kind often seen in noisy data. Instead, we’re mostly concerned with isolated sensor errors in which a single time point may be recorded incorrectly. This most often takes the form of a sensor appearing to teleport to another location for a single sample. To emphasize this goal (removing sensor errors, specifically), we’ll call this procedure “deteleporting”. These “teleports” are generally very apparent on visual inspection, but we would still like an automated method to remove them.

The approach taken by seacurve (which will likely change in the future) uses a local, iterative cubic regression to identify and remove outlying datapoints. The model is applied not to the dual quaternion trajectory directly, but to the speed of the sensor (the norm of it’s positional velocity vector, normalized by the elapsed time). We start by choosing a window win and a threshold thresh in the interval (0,1). The trajectory is then partitioned in win blocks of equal size, and a model

y = f(time) + e

is fit to each, where f is a cubic polynomial. For each observation, we then compute the Cook’s distance (the effect of removing that observation from the model), and reject those observations with a quantile greater than 1 - thresh. Effectively, we perform a one-tailed z-test with threshold thresh.

To see this at work, we can create a new trajectory where the position of the sensor has been perturbed at a single time point.

# Perturb single observation
v <- data$RtmbS2$Position
v[55,] <- v[55,] + c(-.2,.21,.2)

# Convert to seacurve and reorient
x <- sc.posrot2dq(v = v, q = data$RtmbS2$Rotation,
                  timestamps = data$RtmbS2$Timestamps)
x <- sc.orient(x)
sc.plot(x, 'Position')
## Warning: Removed 24 rows containing missing values (geom_point).

Now, we perform the above outlier removal procedure using win = 5 windows and a threshold of thresh = .05:

x <- sc.deteleport(x, win = 5, thresh = .05)
sc.plot(x, 'Position')
## Warning: Removed 48 rows containing missing values (geom_point).

3.5 Interpolation

Seathree performs interpolation directly on the dual quaternion trajectory using dual linear blending (DLB). Effectively, it linearly interpolates between adjacent dual quaternions, and then normalizes the result to have norm equal to 1. Although more accurate forms of interpolation exist, motion trajectories are generally sampled at very high temporal resolution, and so the distortions introduced by this form of interpolation are minimal.

The function sc.interpolate() works in three stages: First, any missing observations at the beginning of the trajectory are set to the earliest available observation. Second, any missing values as the end of the trajectory are set to the latest available observations. Finally, any intermediate missing values are interpolated using DLB. To see this at work:

x <- sc.interpolate(x)
sc.plot(x, 'Position')

If the trajectory does not contain any observations, the function will return an error.

3.6 Preprocessing

For convenience, Seacurve contains a wrapper function sc.process() which accepts an argument sensor, containing a list with elements (v, q, timestamps) to be passed to sc.posrot2dq(), and arguments win and thresh to be passed to sc.deteleport(). The function then applies

sc.posrot2dq(v,q,timestamps) -> sc.orient() -> sc.deteleport(..., win, thresh) -> sc.interpolate()

before finally renormalizing each observation (in case of numerical error), and returning a processed Seacurve object. For example:

filename <- 'data/MDim1_s1_b1_0000.exp'
data <- ImportMotionData(filename, timestamps = c(-.5, 1.5), na.symbol = 0)
x <- sc.process(data$RtmbS2, win = 5, thresh = .05)
sc.plot(x, 'DQ')

The sc.process() function is fairly inflexible, since it’s designed to work with the way my own data are generally formated, and my own preprocessing pipeline, so it may be worthwhile to design your own wrapper if your needs are substantially different. In the event of an error during preprocessing, the function will return a Seacurve object containing all missing (NA) values, which may also be undesirable depending on your workflow.