Resampling of signals recorded with Android sensors
Difficulty Level:
Tags other☁android☁opensignals mobile☁resampling

The OpenSignals mobile application allows to acquire data from the sensors that are built into the hardware of an Android smartphone.

However, when recording data from Android sensors it has to be taken into account that the Android system does not allow recording data with a fixed sampling rate. The Android system rather acquires data based on so called "sensor events". When the system picks up on one of these events it will take a sample from the sensor. These events, however, may not occur at fixed time intervals and thus result in a non-equidistant sampling.
If you prefer to have a more detailed look on how Android acquires data from their sensors you can have a look at the Android Developers guide on sensors . (See the section on "Monitoring Sensor Events" .)

In order to obtain an equidistant sampling with a fixed rate, the signal has to be resampled with an appropriate interpolation method. We will show how this can be achieved in this Jupyter Notebook .

Please take note: The information given in this notebook does only apply when you acquired data from one sensor only . In case you have recorded data from multiple sensors and you want to re-sample the data, there are other important steps that need to be considered. For this, please read our notebook on Synchronising data from multiple Android sensor files into one file . However, we would still encourage you to read through the current notebook in order to understand what effects re-sampling will have on your signals.

In case this is your first time working with Android sensors, we highly recommend reading the Introduction to Android sensors notebook with general information on Android sensors.


1 - Package imports

First, lets import some useful libraries that will be used for data processing purposes.

In [1]:
# package in order to load .txt files
from numpy import loadtxt, zeros, diff, mean, arange, ceil

# package for signal interpolation
from scipy.interpolate import interp1d
In [2]:
# biosignalsnotebooks package
import biosignalsnotebooks as bsnb

# package for plotting the data
from bokeh.plotting import figure, show

2 - A look at the data as it is returned by the OpenSignals mobile app

Before we begin with the resampling, we will have a look at the data structure that is returned from an Android sensor by the OpenSignals mobile application . For this Jupyter Notebook a look at the accelerometer sensor is taken. However, the procedure is the same for any Android sensor type.

In the cell below, the data is shown in its .txt file format. The file has the usual OpenSignals format, containing a header and then displaying the recorded data. From the "column" field in the header we can see that the last three columns are the x, y, and z channels of the accelerometer.

The first column indicates at what time the Android system recorded the sensor event. The time shown in this column is in the time-format that the Android system returns whenever it records a sensor event. It is the time elapsed , in nanoseconds, since the phone was booted.

In [3]:
# Embedding of .pdf file
from IPython.display import IFrame
IFrame(src="../../images/other/android_signal_resampling/opensignals_ANDROID_ACCELEROMETER_2020-07-15_11-33-21.txt", width="100%", height="350")
Out[3]:

The first steps that must be followed should contemplate the load of our data from file, generation of a common time-axis and selection of the relevant channels and time-windows to be analysed:

2.1 - Load data from file

In [4]:
# set file path (this file path needs to be set accoridng to where you saved the data)
file_path="../../images/other/android_signal_resampling/opensignals_ANDROID_ACCELEROMETER_2020-07-15_11-33-21.txt"

# load the data from the file
data = loadtxt(file_path)

2.2 - Generate a common time-axis

The time axis will be shifted so that the recording begins at zero. Additionally an unit conversion (from nanoseconds to seconds) also takes place.

In [5]:
# get timestamp column
raw_time = data[:,0]

# get the start of the acquisition
acq_start = raw_time[0]

# shift time_axis
time_axis = (raw_time - acq_start)

# convert from nanoseconds to seconds
time_axis = time_axis * 1e-9

2.3 - Selection of relevant channels and respective window to be analysed

For visualisation purposes we will only plot the the first 100 samples of the accelerometer"s z axis.

In [6]:
# get z axis of accelerometer
z_acc = data[:,3]

# set the number of samples to be shown
num_samples = 100

# get the first 100 samples
time_axis_samples = time_axis[:num_samples]
z_acc_samples = z_acc[:num_samples] 

Now lets have a look at the data in a plot!

Additionally, we will shift the time axis so that the recording begins at zero and we will convert it from nanoseconds to seconds

Looking at the plot that is generated, we can clearly see that the signal is not equidistantly sampled.

In [7]:
# array with zeros (for plotting the vertical lines in the plot. These are only used for visualisation purposes)
line_start = zeros(num_samples)

# plot the first 100 samples
p = figure()
p.segment(time_axis_samples, line_start, time_axis_samples, z_acc_samples, color=bsnb.opensignals_color_pallet(), line_width=1) # draw lines
p.circle(time_axis_samples, z_acc_samples, color=bsnb.opensignals_color_pallet(), size=10) # draw circles
p.xaxis.axis_label = 'Time (s)'
p.yaxis.axis_label = 'Z-Accelerometer (m/s²)'
bsnb.opensignals_style([p]) # apply biosignalsnotebooks style
show(p)

3 - Calculating the approximate sampling rate

Now we will take a look at how to calculate the approximate sampling rate of the sensor. In order to do this, we will calculate the mean distance between sampling points and then calculate how many samples with this mean distance fit into one second.

As we we will see, the sensor samples on average with approximately 99.59 Hz .

In [8]:
# calculate the distance between sampling points
sample_dist = diff(time_axis)

# calculate the mean distance
mean_dist = mean(sample_dist)

# calculate the number of samples within 1 second
samples_per_second = 1/mean_dist
In [9]:
# print the mean
print('Mean distance between sampling points: {}'.format(mean_dist))

# print the number of samples
print('Approximate sampling rate: {} Hz'.format(samples_per_second))
Mean distance between sampling points: 0.010040886865021772
Approximate sampling rate: 99.59279627814347 Hz

4 - Resampling the signal

Now that we have seen that the signal is not equidistantly sampled and know what is the approximate sampling rate, we can resample the signal to a reasonable sampling rate.
In this instance we are going to use a sampling rate of 100 Hz .

In order to resample the signal, we will use one of scipy"s interpolation methods . To explain the method very briefly, it creates a function which allows for interpolation of the data to a desired range. Additionally, the interpolation method allows for setting which kind of interpolation is used. In order to keep things simple, we will just use linear interpolation, but feel free to try around a little bit with other interpolation functions.

In [10]:
# get the end of the acquisition
acq_end = time_axis[-1]

# create interpolation function
inter_func = interp1d(time_axis, z_acc, kind='linear')

# set sampling frequency
sample_freq = 100

# create new time axis
time_axis_inter = arange(0, acq_end, 1/sample_freq)

# interpolate the values
z_acc_inter = inter_func(time_axis_inter)

Lets plot the signal again and see the result. As we will see, the signal is now equidistantly sampled.

In [11]:
# get the first 100 samples
time_axis_samples_inter = time_axis_inter[:num_samples]
z_acc_samples_inter = z_acc_inter[:num_samples] 

# plot the first 100 samples
p = figure()
p.segment(time_axis_samples_inter, line_start, time_axis_samples_inter, z_acc_samples_inter, color=bsnb.opensignals_color_pallet(), line_width=1) # draw lines
p.circle(time_axis_samples_inter, z_acc_samples_inter, color=bsnb.opensignals_color_pallet(), size=10) # draw circles
p.xaxis.axis_label = 'Time (s)'
p.yaxis.axis_label = 'Z-Accelerometer (m/s²)'
bsnb.opensignals_style([p]) # apply biosignalsnotebooks style
show(p)

However, we need to keep in mind, that the interpolation leads to a slight shift of our data points. The shift of the points differs depending on the kind of interpolation method used.
We can see these shifts when we compare both signals in the same plot, as seen below. The original signal is shown in orange while the interpolated signal is shown in red . As before, we are only showing the first 100 samples of each signal.

Try running the code with different interpolation methods and see how the interpolated signal changes with the kind of interpolation method used.

In [12]:
p = figure()
p.line(time_axis_samples, z_acc_samples, color=bsnb.opensignals_color_pallet(), legend_label="original") # draw original signal
p.line(time_axis_samples_inter, z_acc_samples_inter, color=bsnb.opensignals_color_pallet(), legend_label="interpolated") # draw interpolated signal
p.xaxis.axis_label = 'time (s)'
p.yaxis.axis_label = 'Z-Accelerometer (m/s²)'
bsnb.opensignals_style([p]) # apply biosignalsnotebooks style
show(p)

5 - Resampling all sensor channels at once using the biosignalsnotebooks package

Doing the interpolation manually for each column might seem a bit redundant. Thus, the biosignalsnotebooks package has a function that handles all the work for you. This function uses the same mechanisms as we have implemented above, but is also able to handle multiple data channels. In case you are interested in the implementation, you are welcome to visit our GitHub biosignalsnotebooks repository. This link will direct you exactly to the correct code snippet.

The function is called re_sample_data(...) and it takes the following parameters:

  • time (N, array_like): A 1D array containing the original time axis of the data
  • data (...,N,..., array_like): A N-D array containing data columns that are supposed to be interpolated. The length of data along the interpolation axis has to be the same size as time.
  • start (int, optional): The sample from which the interpolation should be started. When not specified the interpolation starts at 0. When specified the signal will be cropped to this value.
  • stop (int, optional): The sample at which the interpolation should be stopped. When not specified the interpolation stops at the last value. When specified the signal will be cropped to this value.
  • shift_time_axis (bool, optional): If true the time axis will be shifted to start at zero and will be converted to seconds.
  • sampling_rate (int, optional): The sampling rate to which the signal should be resampled. The value should be > 0. If not specified the signal will be resampled to the next tens digit with respect to the approximate sampling rate of the signal (i.e. approx. sampling of 99.59 Hz will be resampled to 100 Hz).
  • kind_interp (string, optional): Specifies the kind of interpolation method to be used as string. If not specified, "linear" interpolation will be used. Available options are: "linear", "nearest", "zero", "slinear", "quadratic", "cubic", "previous", "next" .
The way we call the function below will do the same re-sampling to the z-axis of the accelerometer as we have done above, while also re-sampling the x- and y-axis. This function can also be used for any other Android sensor type, as long as it has more than one sampling point.

In [13]:
# resample all three acceleromter channels at once (the accelerometer data are columns 1-3)
time_inter, data_inter, sampling_rate_inter = bsnb.re_sample_data(raw_time, data[:,1:], shift_time_axis=True)
In [14]:
# plot the z-axis data for the first 100 samples
p = figure()
p.segment(time_inter[:num_samples], line_start, time_inter[:num_samples], data_inter[:num_samples, -1], color=bsnb.opensignals_color_pallet(), line_width=1) # draw lines
p.circle(time_inter[:num_samples], data_inter[:num_samples, -1], color=bsnb.opensignals_color_pallet(), size=10) # draw circles
p.xaxis.axis_label = 'Time (s)'
p.yaxis.axis_label = 'Z-Accelerometer (m/s²)'
bsnb.opensignals_style([p]) # apply biosignalsnotebooks style
show(p)

In this Jupyter notebook we learned how to resample signals from sensors integrated into the hardware of an Android smartphone and how to use the biosignalsnotebooks package in order to re-sample all channels of an Android sensor.

We hope that you have enjoyed this guide . biosignalsnotebooks is an environment in continuous expansion, so don"t stop your journey and learn more with the remaining Notebooks .

In [15]:
from biosignalsnotebooks.__notebook_support__ import css_style_apply
css_style_apply()
.................... CSS Style Applied to Jupyter Notebook .........................
Out[15]: