Commit c229abf8 authored by dhomm's avatar dhomm
Browse files

add combined sensor finder to imustat

parent 651bff13
......@@ -55,22 +55,25 @@ For the wanted placement: forearm the sensor with the metadata placement: fore
All available metadata placements were: ['chest', 'forearm', 'head', 'shin', 'thigh', 'upperarm', 'waist']
The PLOE-Adaption and the Combined-Sensor Finder are also included in the imustat bei P. Scholl. <br>
For details how to use the imustat check https://github.com/pscholl/imustat <br>
Additional to the original imustat two flags can be set: <br>
1. -w for wrist destinction <br>
Example: <br>
./imustat -w ../Thesis-Code/Proband3.mkv <br>
will result in: <br>
Given Placement: forearm - PLOE-placement: (('Position L1, X -> Elbow, Y -> Thumb, Z -> Palm', 1.0), 0.9, (9, 3.5), 1) <br>
+ the result of the imustat <br>
2. -c for the combined sensor finder <br>
Example:
./imustat -c ../Thesis-Code/Proband3.mkv <br>
will result in: <br>
For the wanted placement: wrist the sensor with the metadata placement: forearm was chosen.<br>
All available metadata placements were: ['chest', 'forearm', 'head', 'shin', 'thigh', 'upperarm', 'waist']<br>
+ the result of the imustat <br>
The PLOE-Adaption is also included in the edited imustat.
Note: if -c is chosen during the process the wanted placement is asked for as input string. Please input this placement during the process.
All systems need the av.io module from https://github.com/pscholl/PyAV
......
# Default ignored files
/shelf/
/workspace.xml
imustat
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="NUMPY" />
<option name="myDocStringFormat" value="NumPy" />
</component>
</module>
\ No newline at end of file
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyChainedComparisonsInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoreConstantInTheMiddle" value="true" />
</inspection_tool>
<inspection_tool class="PyPep8Inspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
<option value="W29" />
<option value="E501" />
</list>
</option>
</inspection_tool>
</profile>
</component>
\ No newline at end of file
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9" project-jdk-type="Python SDK" />
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/edited-imustat.iml" filepath="$PROJECT_DIR$/.idea/edited-imustat.iml" />
</modules>
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>
\ No newline at end of file
This diff is collapsed.
imuestat - classify inertial data stream by statistical properties
==================================================================
This repository holds scripts to extract features from the dataset mentioned in
[1], as well as the extracted feature in a CSV file. It's purpose is to allow
easy reproduction of the mentioned results.
## Results
The results of the run as described in the paper can be found in the 'results'
file.
## Usage
The imustat script can be used to classify streams and extract the features.
It's output is in csv format, and it takes .mkv (or other formats that ffmpeg
can read) and analyzes the contained streams. You can turn on debugging which
will print additional information on each row:
```bash
./imustat -d b6276bb5-168d-4b84-a2cd-88b35a5ad354.mkv
acc acc acc 1.000 1.00000 1.00000 66.72874 "b6276bb5-168d-4b84-a2cd-88b35a5ad354.mkv" "none"
gyr gyr gyr 0.008 6.00000 -4.00000 32.34980 "b6276bb5-168d-4b84-a2cd-88b35a5ad354.mkv" "none"
mag mag mag 0.797 3.00000 -1.00000 1.08561 "b6276bb5-168d-4b84-a2cd-88b35a5ad354.mkv" "none"
none none none 0.977 2.00000 0.00000 15.03113 "b6276bb5-168d-4b84-a2cd-88b35a5ad354.mkv" "none"
```
The meaning of each row is:
1. ground truth sensor type (value of NAME tag in stream)
1. estimated sensor type (simple ruleset)
1. estimated sensor type (accmag ruleset)
1. empirical mode of the distribution (value)
1. number of peaks
1. difference of mean number of peaks of none-gyro streams and number of peaks
1. kurtosis of data ditribution
1. input filename
1. value of POSITION tag in input stream
Without '-d' only the results of the classification will be printed
```bash
./imustat b6276bb5-168d-4b84-a2cd-88b35a5ad354.mkv
acc
gyr
mag
none
```
## Requirements
This software requires a recent version of ffmpeg, numpy, matplotlib and scipy. For I/O the [ffmpeginput](github.com/pscholl/ffmpeginput) module is required.
## Caveats
The only tested input format are .mkv file at the moment. Should the
identified sensor type not match your exception, then please check whether your
data recording was interpolated with non-NaN values.
[1]: Philipp M. Scholl, Kristof van Laerhoven - "On The Statistical Properties
of Body-Worn Inertial Motion Data" in Internatiol Symposium on Wearable
Computing, 2017
#!/usr/bin/env python
"""
For different body parts I propose a linter wich checks for the expected
placement different characteristics, which result in thresholds for different
features. Depending on the actual values of these features the linter estimates
the placement and
"""
from av.io import read
import numpy as np
from numpy import mean, sqrt, square, var, corrcoef, std, quantile, percentile
from scipy.stats import skew, kurtosis
def placement_decider(walking_acc, walking_gyro, freq=100, walking_mag=0):
"""
This placement decider takes some statistical moments
to decide which body_part is the most probably
:args:
walking_gyro: 3D data, values in radians/s
walking_acc: 3D data, values in m/s²
:return:
"""
# walking sequences of accelerometer and gyroscope need to be same length
if len(walking_acc) != len(walking_gyro):
length = min(len(walking_acc), len(wakling_gyro))
walking_acc = walking_acc[:length]
walking_gyro = walking_gyro[:length]
# if freq is included as info in the stream take it, otherwhise take the argument
# get freq in Hz, frequency of accelerometer and gyroscope should be the same
acc = walking_acc
gyr = walking_gyro
# only features referring to the magnitude of a sensor will be used
# therefore, they are invariant of orientation ....
norm_acc = [np.linalg.norm(x) for x in acc]
norm_gyr = [np.linalg.norm(x) for x in gyr]
var_acc = var(norm_acc)
var_gyr = var(norm_gyr)
mean_acc = mean(norm_acc)
mean_gyr = mean(norm_gyr)
skew_gyr = skew(norm_gyr)
max_gyr = max(norm_gyr)
# peaks per second of accelerometer > 18 m/s² and gyroscope > 3 rad/sec
pps_acc = len([i for i in norm_acc if i > 18])/(len(norm_acc)/freq)
pps_gyr = len([i for i in norm_gyr if i > 3])/(len(norm_gyr)/freq)
# distinguishing between body_parts with more or with fewer movement
if mean_acc > 10.5 or mean_gyr > 1.05:
# upperarm, forearm, thigh, shin
if pps_acc > 2:
# thigh or shin
if pps_acc > 9 or var_gyr > 3:
return "shin"
else:
return "thigh"
else:
if pps_gyr > 2.8 or mean_gyr > 1.7:
return "forearm"
else:
return "upper arm"
else:
# features leading to only one possibility
# print("skew", skew_gyr,"max gyr", max_gyr, "pps_acc:", pps_acc)
if skew_gyr > 2:
return "head"
if max_gyr > 4.5:
# head, upperarm
if pps_gyr > 0.1: # or pps_acc < 0.25:
if mean_acc > 10.2:
return "upper arm"
else: # mean_acc < 9.9:
return "head"
#elif pps_acc > ... vmtl eher schlecht
# difference between chest and waist is still verry uncertain
else:
return "waist"
else:
if pps_acc > 0.25:
return "waist"
else:
if pps_gyr > 0.0005:
pass
return "chest"
#!/usr/bin/env python
import numpy as np
from numpy import mean, var
from scipy.stats import skew, kurtosis
from av.io import read
import matplotlib.pyplot as plt
# get walking data via subtitle
def get_walking_sequences(sub, freq):
"""
"""
for act in sub[0]:
# since labels can vary, decision that the probability of sit and stand in
# sitting or standing
if "walk" in act[2].lower():
return int(act[0]/(1000/freq)), int(act[1]/(1000/freq))
return 0, -1
def calc_features(acc_stream, gyr_stream, features=["all"], freq=0):
"""
This function calculates the wanted features of the acceleration and angular velocity
:args:
acc_stream: acceleration data stream
gyr_stream: angular velocity data stream
features: list including all features needed to be calculated
default=["all"] => all features wants to be calcualated
freq = the frequency as int of accelerometer and gyroscope, if the stream does not include a sample_rate
:return:
Dcitionary with all calculated features as key and all features as value
"""
norm_acc = [np.linalg.norm(sample) for sample in acc_stream]
norm_gyr = [np.linalg.norm(sample) for sample in gyr_stream]
# get the frequency, acc and gyr should have the same
try:
freq = acc_stream.info.sample_rate
except:
freq = freq
# calculate all wanted features
if features[0] == "all":
mean_acc = mean(acc)
mean_gyr = mean(gyr)
var_acc = var(norm_acc)
var_gyr = var(norm_gyr)
max_gyr = max(norm_gyr)
skew_gyr = skew(norm_gyr)
pps_acc = len([i for i in norm_acc if i > 18])/(len(norm_acc)/freq)
pps_gyr = len([i for i in norm_gyr if i > 3])/(len(norm_gyr)/freq)
return {"mean_acc": mean_acc, "mean_gyr": mean_gyr, "var_acc": var_acc, "var_gyr": var_gyr,
"max_gyr": max_gyr, "skew_gyr": skew_gyr,"pps_acc": pps_acc,"pps_gyr": pps_gyr}
else:
f = {}
if "mean_acc" in features:
mean_acc = mean(norm_acc)
f["mean_acc"] = mean_acc
if "mean_gyr" in features:
mean_gyr = mean(norm_gyr)
f["mean_gyr"] = mean_gyr
if "var_acc" in features:
var_acc = var(norm_acc)
f["var_acc"] = var_acc
if "var_gyr" in features:
var_gyr = var(norm_gyr)
f["var_gyr"] = var_gyr
if "max_gyr" in features:
max_gyr = max(norm_gyr)
f["max_gyr"] = max_gyr
if "skew_gyr" in features:
skew_gyr = skew(norm_gyr)
f["skew_gyr"] = skew_gyr
if "pps_acc" in features:
pps_acc = len([i for i in norm_acc if i > 18])/(len(norm_acc)/freq)
f["pps_acc"] = pps_acc
if "pps_gyr" in features:
pps_gyr = len([i for i in norm_gyr if i > 3])/(len(norm_gyr)/freq)
f["pps_gyr"] = pps_gyr
return f
def paired_acc_gyr(sensor_list, position_key):
"""
This function takes a list with sensors and combines every accelerometer
with it's gyroscope, paired by same metadata[position_key]
:args:
sensor_list: List with accelerometer and gyrsocope sensor streams
:return:
List with tuples of accelerometer and gyroscope streams
"""
# print("paired")
for s in sensor_list:
if s.info.metadata["NAME"].lower() in "accelerometer":
acc = s
for s2 in sensor_list:
if s2.info.metadata["NAME"].lower() in "gyroscope" and \
s2.info.metadata[position_key] == acc.info.metadata[position_key]:
gyr = s2
yield acc, gyr
def get_acc_gyr(sensor_list):
res = []
for s in sensor_list:
if "acc" in s.info.metadata["NAME"].lower() or "gyr" in s.info.metadata["NAME"].lower():
res.append(s)
return res
def feature_decider(a, g, f, w):
"""
This function decides for a given accelerometer and gyroscope stream
if a certain feature is below or above a threshold.
:args:
a: accelerometer stream
g: gyroscope stream
f: triple with feature, t, val, where feature is the feature you want to check
t is "gt" = greater than or "lt" = lower than
val the threshold
w: an integer to adapt the threshold
:return:
Boolean if the feature is as wanted
"""
res = True
for feature, t, val in f:
#print(feature, val, calc_features(a, g, features=[feature])[feature])
if t == "lt":
if not (calc_features(a, g, features=[feature])[feature] < val+w):
res = False
return res
elif t == "gt":
if not (calc_features(a, g, features=[feature])[feature] > val-w):
res = False
return res