ref #27 -new gradle and sdk version

- added faster bpm estimation by using multiple windows now
- implemented a simple z score based outlier detection
- some ugly heuristics to prefend to many -1 on the screen
NOTE: this is a very unstable version of the code, very protoype
This commit is contained in:
toni
2018-04-27 16:03:58 +02:00
parent cad85f8593
commit 85ea37c14b
11 changed files with 282 additions and 39 deletions

View File

@@ -7,7 +7,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
classpath 'com.android.tools.build:gradle:3.1.2'
// NOTE: Do not place your application dependencies here; they belong

View File

@@ -1,6 +1,6 @@
#Tue Dec 19 11:59:51 CET 2017
#Fri Apr 27 11:02:05 CEST 2018
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip

View File

@@ -6,10 +6,14 @@ import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Handler;
import android.util.Log;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Vector;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
@@ -21,6 +25,7 @@ import de.tonifetzer.conductorswatch.bpmEstimation.BpmEstimator;
import de.tonifetzer.conductorswatch.network.SensorDataFileSender;
import de.tonifetzer.conductorswatch.network.SensorDataFileStreamer;
import de.tonifetzer.conductorswatch.utilities.ByteStreamWriter;
import de.tonifetzer.conductorswatch.utilities.Utils;
/**
* Created by toni on 13/11/17.
@@ -34,6 +39,7 @@ public class Estimator implements SensorEventListener {
private Context mContext;
private AccelerometerWindowBuffer mAccelerometerWindowBuffer;
private BpmEstimator mBpmEstimator;
private double mCurrentBpm;
private ByteStreamWriter mByteStreamWriterAcc;
private ByteStreamWriter mByteStreamWriterGyro;
@@ -44,6 +50,7 @@ public class Estimator implements SensorEventListener {
public Estimator(Context mContext){
this.mContext = mContext;
this.mCurrentBpm = -1;
}
public void start() {
@@ -54,7 +61,8 @@ public class Estimator implements SensorEventListener {
mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_FASTEST);
mSensorManager.registerListener(this, mRawAccelerometer, SensorManager.SENSOR_DELAY_FASTEST);
mAccelerometerWindowBuffer = new AccelerometerWindowBuffer(6000, 1500);
mAccelerometerWindowBuffer = new AccelerometerWindowBuffer(6000, 750);
mBpmEstimator = new BpmEstimator(mAccelerometerWindowBuffer, 0, 5000);
mTimer.scheduleAtFixedRate(new TimerTask() {
@@ -62,11 +70,89 @@ public class Estimator implements SensorEventListener {
public void run() {
if (mAccelerometerWindowBuffer.isNextWindowReady()) {
double bpm = mBpmEstimator.estimate(mAccelerometerWindowBuffer.getFixedWindow());
LinkedList<Double> bpmList = new LinkedList<>();
//todo: wie viele dieser Klassen kann ich wegwerfen um das Ergebnis nich schlechter zu machen?
double bpm60 = mBpmEstimator.estimate(mAccelerometerWindowBuffer.getFixedSizedWindow());
double bpm85 = mBpmEstimator.estimate(mAccelerometerWindowBuffer.getFixedSizedWindow(3500, 750));
double bpm110 = mBpmEstimator.estimate(mAccelerometerWindowBuffer.getFixedSizedWindow(2600, 750));
double bpm135 = mBpmEstimator.estimate(mAccelerometerWindowBuffer.getFixedSizedWindow(2000, 750));
double bpm160 = mBpmEstimator.estimate(mAccelerometerWindowBuffer.getFixedSizedWindow(1600,750));
double bpm200 = mBpmEstimator.estimate(mAccelerometerWindowBuffer.getFixedSizedWindow(1200, 750));
//add to list
//todo: make this cool...
//vielleicht einen weighted mean?
bpmList.add(bpm60);
bpmList.add(bpm85);
bpmList.add(bpm110); //110, 135, 160 langen auch schon
bpmList.add(bpm135);
bpmList.add(bpm160);
bpmList.add(bpm200);
//Log.d("BPM: ", bpmList.toString());
//remove all -1 and calc bpmMean
while(bpmList.remove(Double.valueOf(-1))) {}
//remove outliers
//todo: aktuell wird die liste hier sortiert.. eig net so schön.
Utils.removeOutliersZScore(bpmList, 3.4);
//Utils.removeOutliersHeuristic();
//Log.d("BPM: ", bpmList.toString());
double bpm = -1;
if(!bpmList.isEmpty()) {
double bpmMean = Utils.mean(bpmList);
double bpmMedian = Utils.median(bpmList);
double bpmDiffSlowFast = bpmList.getFirst() - bpmList.getLast();
if (Math.abs(bpmDiffSlowFast) > 25) {
double tmpBPM = bpmMean + 25;
while(bpm == -1){
if (tmpBPM < 60) {
bpm = bpm60;
if(bpm == -1){
bpm = bpmMean;
}
}
else if (tmpBPM < 85) {
bpm = bpm85;
} else if (tmpBPM < 110) {
bpm = bpm110;
} else if (tmpBPM < 135) {
bpm = bpm135;
} else if (tmpBPM < 160) {
bpm = bpm160;
} else {
bpm = bpm200;
}
tmpBPM -= 5;
}
//Log.d("BPM: ", "CHANGE");
} else {
bpm = bpmMean;
//Log.d("BPM: ", "STAY");
}
}
for (OnBpmEstimatorListener listener : listeners) {
listener.onNewDataAvailable(bpm);
listener.onNewDataAvailable(bpm); //135 gibt gute ergebnisse!
}
//update the windowSize and updaterate depending on current bpm
//updateWindowSizeAndOverlap(bpm);
}
}
}, 0, 100);
@@ -134,4 +220,39 @@ public class Estimator implements SensorEventListener {
private List<OnBpmEstimatorListener> listeners = new CopyOnWriteArrayList<OnBpmEstimatorListener>();
public void add(OnBpmEstimatorListener listener){listeners.add(listener);}
public void remove(OnBpmEstimatorListener listener){listeners.remove(listener);}
/**
* Simple function that sets die windowSize and Overlap time to a specific value
* depending on the currentBPM. Nothing rly dynamical. However, should work out
* for our purposes.
* @param bpm
*/
private void updateWindowSizeAndOverlap(double bpm){
//round to nearest tenner. this limits the number of windowsize and overlap changes.
int newBpmRounded = (int) Math.round(bpm / 10.0) * 10;
int curBpmRounded = (int) Math.round(mCurrentBpm / 10.0) * 10;
//TODO: i guess this is not the best method.. if the default sizes always produces -1, we run into problems
if(bpm != -1 && curBpmRounded != newBpmRounded){
int overlap_ms = 60000 / newBpmRounded;
int window_ms = overlap_ms * 5;
//idea: wenn man mehrere fenster parallel laufen lässt und beobachtet, müsste das kleinste fenster die tempowechsel eigentlich am
// besten mitbekommen. dieses fenster dann bestimmen lassen?
mAccelerometerWindowBuffer.setWindowSize(window_ms);
mAccelerometerWindowBuffer.setOverlapSize(overlap_ms);
} else if (bpm == -1){
//if bpm is -1 due to a non-classification, reset to default.
//idea: anstatt auf einen festen wert zu setzen, könnte man das fenster dann auch einfach ein wenig größer / kleiner machen.
mAccelerometerWindowBuffer.setWindowSize(3000);
mAccelerometerWindowBuffer.setOverlapSize(750);
}
mCurrentBpm = bpm;
}
}

View File

@@ -307,6 +307,8 @@ public class MainActivity extends WearableActivity implements WorkerFragment.OnF
@Override
protected void onStart() {
super.onStart();
mSender.wakeUpPhoneCall();
}
@Override

View File

@@ -144,14 +144,14 @@ public class WorkerFragment extends Fragment implements Metronome.OnMetronomeLis
public void onNewDataAvailable(double bpm) {
//TODO: what if multiple threads access mBpmList? put into synchronized? @frank fragen :D
//TODO: send this to smartphone
if(mWorkerRunning){
mBpmList.add(bpm);
if(bpm == -1){
//to stuff with UI. Write Text or make XX or something like that
}
mBpmList.add(bpm);
// we need this here, since ui elements can only be changed within activity thread and
// onNewDataAvailable lives inside the bpm estimation thread.
// TODO: is this really okay? also synchronized?

View File

@@ -2,6 +2,7 @@ package de.tonifetzer.conductorswatch.bpmEstimation;
import de.tonifetzer.conductorswatch.utilities.Utils;
import java.util.ArrayList;
import java.util.List;
/**
* Created by toni on 15/12/17.
@@ -34,23 +35,37 @@ public class AccelerometerWindowBuffer extends ArrayList<AccelerometerData> {
//add element
boolean r = super.add(ad);
if ((getYongest().ts - getOldest().ts) > mWindowSize){
removeOldElements();
long oldestTime = getYongest().ts - mWindowSize;
for(int i = 0; i < size(); ++i) {
if (get(i).ts > oldestTime) {
break;
}
remove(i);
}
}
return r;
}
}
private void removeOldElements(){
synchronized (this) {
if (!isEmpty()) {
if ((getYongest().ts - getOldest().ts) > mWindowSize) {
long oldestTime = getYongest().ts - mWindowSize;
for (int i = 0; i < size(); ++i) {
if (get(i).ts > oldestTime) {
break;
}
super.remove(i);
}
}
}
}
}
public boolean isNextWindowReady(long lastWindowTS, int overlapSize){
return true;
}
public boolean isNextWindowReady(){
if(!isEmpty()){
if(((getYongest().ts - getOldest().ts) > mWindowSize / 2) && mOverlapCounter > mOverlapSize){
if(((getYongest().ts - getOldest().ts) > mWindowSize / 4) && mOverlapCounter > mOverlapSize){
mOverlapCounter = 0;
return true;
@@ -59,22 +74,40 @@ public class AccelerometerWindowBuffer extends ArrayList<AccelerometerData> {
return false;
}
public AccelerometerWindowBuffer getFixedWindow(){
AccelerometerWindowBuffer other = new AccelerometerWindowBuffer(mWindowSize, mOverlapSize);
synchronized (this){
for(AccelerometerData data : this){
other.add(new AccelerometerData(data));
}
public AccelerometerWindowBuffer getFixedSizedWindow(int size, int overlap){
AccelerometerWindowBuffer other = new AccelerometerWindowBuffer(size, overlap);
double sampleRate = ((getYongest().ts - getOldest().ts) / super.size());
//if current size is smaller then wanted size, start at 0 and provide smaller list
int start = 0;
if((getYongest().ts - getOldest().ts) > size){
start = (int) Math.round(super.size() - (size / sampleRate));
}
// start should not be negative, this can happen due to rounding errors.
start = start < 0? 0 : start;
synchronized (this) {
other.addAll(super.subList(start, super.size()));
}
return other;
}
public AccelerometerWindowBuffer getFixedSizedWindow(){
return getFixedSizedWindow(mWindowSize, mOverlapSize);
}
//TODO: check if list is empty! this causes indexoutofbounce
public AccelerometerData getYongest() {
return get(size() - 1);
synchronized (this){
return super.get(size() - 1);
}
}
public AccelerometerData getOldest() {
return get(0);
return super.get(0);
}
public double[] getX(){
@@ -96,4 +129,22 @@ public class AccelerometerWindowBuffer extends ArrayList<AccelerometerData> {
public int getOverlapSize(){
return mOverlapSize;
}
public void setWindowSize(int size_ms){
this.mWindowSize = size_ms;
removeOldElements(); // need to call this here, to remove too old elements, if the windowSize gets smaller.
}
public void setOverlapSize(int size_ms){
this.mOverlapSize = size_ms;
this.mOverlapCounter = 0;
}
@Override
public void clear(){
synchronized (this){
super.clear();
this.mOverlapCounter = 0;
}
}
}

View File

@@ -40,6 +40,9 @@ public class BpmEstimator {
mResetCounter = 0;
mResetLimit_ms = resetAfter_ms;
//TODO: this is to easy. since the new dyn. windowsize produces smaller update times, we need to consider something, that
//TODO: holds more values, if they are similar, an resets the history if not.
mMvg = new MovingFilter(2);
//mKalman = new SimpleKalman();
}
@@ -52,6 +55,10 @@ public class BpmEstimator {
sampleRate = Math.round(Utils.mean(Utils.diff(fixedWindow.getTs())));
}
if(sampleRate == 0){
int breakhere = 0;
}
AccelerometerInterpolator interp = new AccelerometerInterpolator(fixedWindow, sampleRate);
//are we conducting?
@@ -80,6 +87,8 @@ public class BpmEstimator {
mMvg.add(estimatedBPM);
mBpmHistory.add(mMvg.getAverage());
//mBpmHistory.add(estimatedBPM);
//moving median (lohnt nur bei konstantem tempo, da wir nur ein tempo damit gut halten können.)
//mMvg.add(estimatedBPM);
//mBpmHistory.add(mMvg.getMedian());
@@ -96,7 +105,7 @@ public class BpmEstimator {
mBpmHistory.clear();
//TODO: send signal to clear.
mBuffer.clear();
//mBuffer.clear();
mMvg.clear();
mResetCounter = 0;
}

View File

@@ -4,8 +4,10 @@ import android.content.Context;
import android.content.res.Resources;
import android.util.DisplayMetrics;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.Iterator;
import java.util.List;
//TODO: change from double to generic type
public class Utils {
@@ -37,7 +39,7 @@ public class Utils {
return sum;
}
public static double sum (LinkedList<Double> data){
public static double sum (List<Double> data){
double sum = 0;
for (int i = 0; i < data.size(); i++) {
sum += data.get(i).doubleValue();
@@ -81,11 +83,11 @@ public class Utils {
return (double) sum(data) / (double) data.length;
}
public static double mean(LinkedList<Double> data){
public static double mean(List<Double> data){
return sum(data) / data.size();
}
public static double median(LinkedList<Double> data){
public static double median(List<Double> data){
data.sort(Comparator.naturalOrder());
double median;
@@ -97,6 +99,18 @@ public class Utils {
return median;
}
public static double mad(List<Double> data){
double median = median(data);
List<Double> tmpList = new ArrayList<>();
for(double value : data){
tmpList.add(Math.abs(value - median));
}
return median(tmpList);
}
//TODO: Could be slow.. faster method?
public static double geometricMean(double[] data) {
double sum = data[0];
@@ -173,4 +187,17 @@ public class Utils {
return px / ((float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT);
}
public static void removeOutliersZScore(List<Double> data, double score) {
if(!data.isEmpty()){
double median = median(data);
double mad = mad(data);
for(Iterator<Double> it = data.iterator(); it.hasNext(); ){
double M = Math.abs((0.6745 * (it.next() - median)) / mad);
if (M > score){ it.remove(); }
}
}
}
}

View File

@@ -10,7 +10,7 @@ buildscript {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
classpath 'com.android.tools.build:gradle:3.1.2'
// NOTE: Do not place your application dependencies here; they belong

View File

@@ -1,6 +1,6 @@
#Mon Nov 13 10:12:40 CET 2017
#Fri Apr 27 14:10:48 CEST 2018
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip

View File

@@ -41,9 +41,9 @@
%measurements = dlmread('../measurements/wearR/recording_180bpm_4-4_177064915.csv', ';'); *
%files = dir(fullfile('../../measurements/lgWear/', '*.csv'));
files = dir(fullfile('../../measurements/lgWear/', '*.csv'));
%files = dir(fullfile('../../measurements/wearR/', '*.csv'));
files = dir(fullfile('../../measurements/peter_failed/', '*.csv'));
%files = dir(fullfile('../../measurements/peter_failed/', '*.csv'));
for file = files'
@@ -52,7 +52,7 @@ for file = files'
%draw the raw acc data
m_idx = [];
m_idx = (measurements(:,2)==10);
m_idx = (measurements(:,2)==2); %Android App: 10, Normal Data: 2
m = measurements(m_idx, :);
%Interpolate to generate a constant sample rate to 250hz (4ms per sample)
@@ -71,13 +71,19 @@ for file = files'
legend("x", "location", "eastoutside");
figure(2);
plot(m(:,1),m(:,4)) %y
plot(m(:,1),m(:,4)) %yt
legend("y", "location", "eastoutside");
figure(3);
plot(m(:,1),m(:,5)) %z
legend("z", "location", "eastoutside");
%magnitude
magnitude = sqrt(sum(m(:,3:5).^2,2));
figure(5);
plot(m(:,1), magnitude);
legend("magnitude", "location", "eastoutside");
waitforbuttonpress();
%save timestamps
@@ -98,26 +104,39 @@ for file = files'
[corr_x, lag_x] = xcov(m(i-window_size:i,3), (window_size/4), "coeff");
[corr_y, lag_y] = xcov(m(i-window_size:i,4), (window_size/4), "coeff");
[corr_z, lag_z] = xcov(m(i-window_size:i,5), (window_size/4), "coeff");
[corr_mag, lag_mag] = xcov(magnitude(i-window_size:i), (window_size/4), "coeff");
%autocorrelation of the autocorrelation?!
%[corr_corr_x, lag_lag_x] = xcov(corr_x, length(corr_x), "coeff");
%[corr_corr_y, lag_lag_y] = xcov(corr_y, length(corr_x), "coeff");
%[corr_corr_z, lag_lag_z] = xcov(corr_z, length(corr_x), "coeff");
corr_x_pos = corr_x;
corr_y_pos = corr_y;
corr_z_pos = corr_z;
corr_mag_pos = corr_mag;
corr_x_pos(corr_x_pos<0)=0;
corr_y_pos(corr_y_pos<0)=0;
corr_z_pos(corr_z_pos<0)=0;
corr_mag_pos(corr_mag_pos<0)=0;
[peak_x, idx_x_raw] = findpeaks(corr_x_pos, 'MinPeakHeight', 0.1,'MinPeakDistance', 50, 'MinPeakProminence', 0.1);
[peak_y, idx_y_raw] = findpeaks(corr_y_pos, 'MinPeakHeight', 0.1,'MinPeakDistance', 50, 'MinPeakProminence', 0.1);
[peak_z, idx_z_raw] = findpeaks(corr_z_pos, 'MinPeakHeight', 0.1,'MinPeakDistance', 50, 'MinPeakProminence', 0.1);
[peak_mag, idx_mag_raw] = findpeaks(corr_mag_pos, 'MinPeakHeight', 0.1,'MinPeakDistance', 50, 'MinPeakProminence', 0.1);
idx_x_raw = sort(idx_x_raw);
idx_y_raw = sort(idx_y_raw);
idx_z_raw = sort(idx_z_raw);
idx_mag_raw = sort(idx_mag_raw);
idx_x = findFalseDetectedPeaks(idx_x_raw, lag_x, corr_x);
idx_y = findFalseDetectedPeaks(idx_y_raw, lag_y, corr_y);
idx_z = findFalseDetectedPeaks(idx_z_raw, lag_z, corr_z);
idx_mag = findFalseDetectedPeaks(idx_mag_raw, lag_mag, corr_mag);
Xwindow = m(i-window_size:i,3);
Xwindow_mean_ts_diff = mean(diff(lag_x(idx_x) * sample_rate_ms)); %2.5 ms is the time between two samples at 400hz
@@ -155,6 +174,20 @@ for file = files'
title(strcat(" ", m_label_ms, " ", m_label_bpm));
hold ("off");
%magnitude
Mwindow = magnitude(i-window_size:i);
Mwindow_mean_ts_diff = mean(diff(lag_mag(idx_mag)* sample_rate_ms));
Mwindow_mean_bpm = (60000 / (Mwindow_mean_ts_diff));
figure(14);
plot(lag_mag, corr_mag, lag_mag(idx_mag), corr_mag(idx_mag), 'r*', lag_mag(idx_mag_raw), corr_mag(idx_mag_raw), 'g*') %z
hold ("on")
m_label_ms = strcat(" mean ms: ", num2str(Mwindow_mean_ts_diff));
m_label_bpm = strcat(" mean bpm: ", num2str(Mwindow_mean_bpm));
title(strcat(" ", m_label_ms, " ", m_label_bpm));
hold ("off");
%breakpoints dummy for testing
if(length(idx_x) > length(idx_x_raw))