ref #9 - verbindung zwischen watch und app geht

- watch startet automatisch auch app
- bpm kommen in hoher geschwindigkeit an
ref #19 - rückweisunsklasse etwas verbessert
- fehlnde files hochgeladen für die bpm estimation
This commit is contained in:
toni
2017-12-19 20:47:28 +01:00
parent 70cf17c479
commit ac602f7ae7
42 changed files with 1570 additions and 0 deletions

9
android/ConductorsWatch/.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.externalNativeBuild

View File

@@ -0,0 +1,31 @@
package de.tonifetzer.conductorswatch.bpmEstimation;
/**
* Created by toni on 15/12/17.
*/
public class AccelerometerData {
public double x,y,z;
public long ts;
public AccelerometerData(long ts, double x, double y, double z){
this.ts = ts;
this.x = x;
this.y = y;
this.z = z;
}
@Override
public boolean equals(Object other){
if (this == other)
return true;
if (!(other instanceof AccelerometerData)) {
return false;
}
AccelerometerData ad = (AccelerometerData) other;
return ts == ad.ts;
}
}

View File

@@ -0,0 +1,108 @@
package de.tonifetzer.conductorswatch.bpmEstimation;
import java.util.Arrays;
/**
* Created by toni on 16/12/17.
*/
public class AccelerometerInterpolator {
private double[] mX;
private double[] mY;
private double[] mZ;
private long[] mTsInterp;
public AccelerometerInterpolator(AccelerometerWindowBuffer ab, double sampleRate_ms){
long size = (ab.getYongest().ts - (ab.getOldest().ts - (long) sampleRate_ms)) / (long) sampleRate_ms;
mTsInterp = new long[(int)size];
int j = 0;
for(long i = ab.getOldest().ts; i <= ab.getYongest().ts; i += sampleRate_ms){
mTsInterp[j++] = i;
}
mX = interpLinear(ab.getTs(), ab.getX(), mTsInterp);
mY = interpLinear(ab.getTs(), ab.getY(), mTsInterp);
mZ = interpLinear(ab.getTs(), ab.getZ(), mTsInterp);
}
public long[] getTs(){
return mTsInterp;
}
public double[] getX(){
return mX;
}
public double[] getY(){
return mY;
}
public double[] getZ(){
return mZ;
}
private static double[] interpLinear(double[] x, double[] y, double[] xi) throws IllegalArgumentException {
if (x.length != y.length) {
throw new IllegalArgumentException("X and Y must be the same length");
}
if (x.length == 1) {
throw new IllegalArgumentException("X must contain more than one value");
}
double[] dx = new double[x.length - 1];
double[] dy = new double[x.length - 1];
double[] slope = new double[x.length - 1];
double[] intercept = new double[x.length - 1];
// Calculate the line equation (i.e. slope and intercept) between each point
for (int i = 0; i < x.length - 1; i++) {
dx[i] = x[i + 1] - x[i];
if (dx[i] == 0) {
throw new IllegalArgumentException("X must be montotonic. A duplicate " + "x-value was found");
}
if (dx[i] < 0) {
throw new IllegalArgumentException("X must be sorted");
}
dy[i] = y[i + 1] - y[i];
slope[i] = dy[i] / dx[i];
intercept[i] = y[i] - x[i] * slope[i];
}
// Perform the interpolation here
double[] yi = new double[xi.length];
for (int i = 0; i < xi.length; i++) {
if ((xi[i] > x[x.length - 1]) || (xi[i] < x[0])) {
yi[i] = Double.NaN;
}
else {
int loc = Arrays.binarySearch(x, xi[i]);
if (loc < -1) {
loc = -loc - 2;
yi[i] = slope[loc] * xi[i] + intercept[loc];
}
else {
yi[i] = y[loc];
}
}
}
return yi;
}
private static double[] interpLinear(long[] x, double[] y, long[] xi) throws IllegalArgumentException {
double[] xd = new double[x.length];
for (int i = 0; i < x.length; i++) {
xd[i] = x[i];
}
double[] xid = new double[xi.length];
for (int i = 0; i < xi.length; i++) {
xid[i] = xi[i];
}
return interpLinear(xd, y, xid);
}
}

View File

@@ -0,0 +1,74 @@
package de.tonifetzer.conductorswatch.bpmEstimation;
import de.tonifetzer.conductorswatch.utilities.Utils;
import java.util.ArrayList;
/**
* Created by toni on 15/12/17.
*/
public class AccelerometerWindowBuffer extends ArrayList<AccelerometerData> {
private int mWindowSize;
private int mOverlapSize;
private int mOverlapCounter;
public AccelerometerWindowBuffer(int windowSize, int overlap){
mWindowSize = windowSize;
mOverlapSize = overlap;
mOverlapCounter = 1;
}
//TODO: add exception handling. falseArgument if ad has no numeric x,y,z
public boolean add(AccelerometerData ad){
//do not add duplicates!
if(!isEmpty() && getYongest().equals(ad)){
return false;
}
boolean r = super.add(ad);
if (size() > mWindowSize){
removeRange(0, size() - mWindowSize);
}
++mOverlapCounter;
return r;
}
public boolean isNextWindowReady(){
if((size() > mWindowSize / 2) && mOverlapCounter > mOverlapSize){
mOverlapCounter = 1;
return true;
}
return false;
}
public AccelerometerData getYongest() {
return get(size() - 1);
}
public AccelerometerData getOldest() {
return get(0);
}
public double[] getX(){
return this.stream().mapToDouble(d -> d.x).toArray();
}
public double[] getY(){
return this.stream().mapToDouble(d -> d.y).toArray();
}
public double[] getZ(){
return this.stream().mapToDouble(d -> d.z).toArray();
}
public long[] getTs(){
return this.stream().mapToLong(d -> d.ts).toArray();
}
public int getOverlapSize(){
return mOverlapSize;
}
}

View File

@@ -0,0 +1,79 @@
package de.tonifetzer.conductorswatch.bpmEstimation;
import org.jtransforms.fft.DoubleFFT_1D;
import de.tonifetzer.conductorswatch.utilities.Utils;
import java.util.Arrays;
/**
* Created by toni on 15/12/17.
*/
public class AutoCorrelation {
private int mMaxLag;
private double[] mCorr;
public AutoCorrelation(double[] data, int maxLag){
mMaxLag = maxLag;
mCorr = fft(data);
}
public double[] getCorr(){
return mCorr;
}
private double[] fft(double[] data) {
if(mMaxLag < 1){
throw new RuntimeException("maxlag has to be greater 1");
}
int n = data.length;
double[] x = Arrays.copyOf(data, n);
int mxl = Math.min(mMaxLag, n - 1);
int ceilLog2 = Utils.nextPow2(2*n -1);
int n2 = (int) Math.pow(2,ceilLog2);
// x - mean(x) (pointwise)
double x_mean = Utils.mean(x);
for(int i = 0; i < x.length; ++i){
x[i] -= x_mean;
}
// double the size of x and fill up with zeros. if x is not even, add additional 0
double[] x2 = new double[n2 * 2]; //need double the size for fft.realForwardFull (look into method description)
Arrays.fill(x2, 0);
System.arraycopy(x,0, x2, 0, x.length);
// x_fft calculate fft 1D
DoubleFFT_1D fft = new DoubleFFT_1D(n2);
fft.realForwardFull(x2);
// Cr = abs(x_fft).^2 (absolute with complex numbers is (r^2) + (i^2)
double[] Cr = new double[n2 * 2];
int j = 0;
for(int i = 0; i < x2.length; ++i){
Cr[j++] = Utils.sqr(x2[i]) + Utils.sqr(x2[i+1]);
++i; //skip the complex part
}
// ifft(Cr,[],1)
DoubleFFT_1D ifft = new DoubleFFT_1D(n2);
ifft.realInverseFull(Cr, true);
// remove complex part and scale/normalize
double[] c1 = new double[n2];
j = 0;
for(int i = 0; i < Cr.length; ++i){
c1[j++] = Cr[i] / Cr[0];
++i; //skip the complex part
}
// Keep only the lags we want and move negative lags before positive lags.
double[] c = new double[(mxl * 2) + 1];
System.arraycopy(c1, 0, c, mxl, mxl + 1); // +1 to place the 1.0 in the middle of correlation
System.arraycopy(c1, n2 - mxl, c, 0, mxl);
return c;
}
}

View File

@@ -0,0 +1,199 @@
package de.tonifetzer.conductorswatch.bpmEstimation;
import de.tonifetzer.conductorswatch.utilities.MovingFilter;
import de.tonifetzer.conductorswatch.utilities.SimpleKalman;
import de.tonifetzer.conductorswatch.utilities.Utils;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
/**
* Created by toni on 17/12/17.
*/
public class BpmEstimator {
private AccelerometerWindowBuffer mBuffer;
private double mSampleRate_ms;
private LinkedList<Double> mBpmHistory_X;
private LinkedList<Double> mBpmHistory_Y;
private LinkedList<Double> mBpmHistory_Z;
private LinkedList<Double> mBpmHistory;
private int mResetCounter;
private int mResetLimit_ms;
private MovingFilter mMvg;
//private SimpleKalman mKalman;
public BpmEstimator(AccelerometerWindowBuffer windowBuffer, double sampleRate_ms, int resetAfter_ms){
mBuffer = windowBuffer;
mSampleRate_ms = sampleRate_ms;
mBpmHistory_X = new LinkedList<>();
mBpmHistory_Y = new LinkedList<>();
mBpmHistory_Z = new LinkedList<>();
mBpmHistory = new LinkedList<>();
mResetCounter = 0;
mResetLimit_ms = resetAfter_ms;
mMvg = new MovingFilter(2);
//mKalman = new SimpleKalman();
}
public double estimate(){
double sampleRate = mSampleRate_ms;
if(sampleRate <= 0){
sampleRate = Math.round(Utils.mean(Utils.diff(mBuffer.getTs())));
}
AccelerometerInterpolator interp = new AccelerometerInterpolator(mBuffer, sampleRate);
//are we conducting?
//just look at the newest 512 samples
//List<AccelerometerData> subBuffer = mBuffer.subList(mBuffer.size() - 512, mBuffer.size());
double[] xAutoCorr = new AutoCorrelation(interp.getX(), 512).getCorr();
double[] yAutoCorr = new AutoCorrelation(interp.getY(), 512).getCorr();
double[] zAutoCorr = new AutoCorrelation(interp.getZ(), 512).getCorr();
Peaks pX = new Peaks(xAutoCorr, 50, 0.1f, 0, false);
Peaks pY = new Peaks(yAutoCorr, 50, 0.1f, 0, false);
Peaks pZ = new Peaks(zAutoCorr, 50, 0.1f, 0, false);
mBpmHistory_X.add(pX.getBPM(sampleRate));
mBpmHistory_Y.add(pY.getBPM(sampleRate));
mBpmHistory_Z.add(pZ.getBPM(sampleRate));
double estimatedBPM = getBestBpmEstimation(pX, pY, pZ);
if(estimatedBPM != -1){
//moving avg (lohnt dann, wenn wir viele daten haben)
mMvg.add(estimatedBPM);
mBpmHistory.add(mMvg.getAverage());
//moving median (lohnt nur bei konstantem tempo, da wir nur ein tempo damit gut halten können.)
//mMvg.add(estimatedBPM);
//mBpmHistory.add(mMvg.getMedian());
//kalman filter (lohnt dann, wenn wir konstantes tempo haben, mit startangabe!)
//standard last element
//mBpmHistory.add(estimatedBPM);
//mResetCounter = 0;
}
else {
int resetAfter = (int) Math.round(mResetLimit_ms / (mBuffer.getOverlapSize() * sampleRate));
if(++mResetCounter > resetAfter){
mBpmHistory.clear();
mBuffer.clear();
mMvg.clear();
mResetCounter = 0;
}
return -1;
}
//last element
return mBpmHistory.getLast();
}
public double getMeanBpm(){
return Utils.mean(mBpmHistory);
}
public double getMedianBPM(){
return Utils.median(mBpmHistory);
}
private double getBestBpmEstimation(Peaks peaksX, Peaks peaksY, Peaks peaksZ) throws IllegalArgumentException {
int cntNumAxis = 0;
double sumCorr = 0; //to prevent division by zero
double sumRms = 0;
double sumNumInter = 0;
double corrMeanX = 0, corrRmsX = 0;
int corrNumInterX = 0;
if(peaksX.hasPeaks()){
corrMeanX = Utils.geometricMean(peaksX.getPeaksValueWithoutZeroIdxAndNegativeValues());
corrRmsX = Utils.rms(peaksX.getPeaksValueWithoutZeroIdx());
corrNumInterX = Utils.intersectionNumber(peaksX.getData(), 0.2f);
++cntNumAxis;
sumCorr += corrMeanX;
sumRms += corrRmsX;
sumNumInter += corrNumInterX;
}
double corrMeanY = 0, corrRmsY = 0;
int corrNumInterY = 0;
if(peaksY.hasPeaks()){
corrMeanY = Utils.geometricMean(peaksY.getPeaksValueWithoutZeroIdxAndNegativeValues());
corrRmsY = Utils.rms(peaksY.getPeaksValueWithoutZeroIdx());
corrNumInterY = Utils.intersectionNumber(peaksY.getData(), 0.2f);
++cntNumAxis;
sumCorr += corrMeanY;
sumRms += corrRmsY;
sumNumInter += corrNumInterY;
}
double corrMeanZ = 0, corrRmsZ = 0;
int corrNumInterZ = 0;
if(peaksZ.hasPeaks()){
corrMeanZ = Utils.geometricMean(peaksZ.getPeaksValueWithoutZeroIdxAndNegativeValues());
corrRmsZ = Utils.rms(peaksZ.getPeaksValueWithoutZeroIdx());
corrNumInterZ = Utils.intersectionNumber(peaksZ.getData(), 0.2f);
++cntNumAxis;
sumCorr += corrMeanZ;
sumRms += corrRmsZ;
sumNumInter += corrNumInterZ;
}
//no peaks, reject
if(cntNumAxis == 0){
//throw new IllegalArgumentException("All Peaks are empty! -> Reject Estimation");
return -1;
}
/*
System.out.println("RMS-X: " + corrRmsX);
System.out.println("GEO-X: " + corrMeanX);
System.out.println("INTER-X: " + corrNumInterX);
System.out.println("RMS-Y: " + corrRmsY);
System.out.println("GEO-Y: " + corrMeanY);
System.out.println("INTER-Y: " + corrNumInterY);
System.out.println("RMS-Z: " + corrRmsZ);
System.out.println("GEO-Z: " + corrMeanZ);
System.out.println("INTER-Z: " + corrNumInterZ);
*/
//values to low, reject
//TODO: this is a pretty simple assumption. first shot!
if(corrRmsX < 0.25 && corrRmsY < 0.25 && corrRmsZ < 0.25){
return -1;
}
double quantityX = ((corrMeanX / sumCorr) + (corrRmsX / sumRms) + (corrNumInterX / sumNumInter)) / cntNumAxis;
double quantityY = ((corrMeanY / sumCorr) + (corrRmsY / sumRms) + (corrNumInterY / sumNumInter)) / cntNumAxis;
double quantityZ = ((corrMeanZ / sumCorr) + (corrRmsZ / sumRms) + (corrNumInterZ / sumNumInter)) / cntNumAxis;
//get best axis by quantity and estimate bpm
if(quantityX > quantityY && quantityX > quantityZ){
return mBpmHistory_X.getLast();
}
else if(quantityY > quantityZ){
return mBpmHistory_Y.getLast();
}
else {
return mBpmHistory_Z.getLast();
}
}
}

View File

@@ -0,0 +1,206 @@
package de.tonifetzer.conductorswatch.bpmEstimation;
import de.tonifetzer.conductorswatch.utilities.Utils;
import java.util.LinkedList;
/**
* Created by toni on 15/12/17.
*/
public class Peaks {
private LinkedList<Integer> mPeaksIdx; //provide the idx within the given data array
private LinkedList<Integer> mPeaksPos; //the real position within the data-rang e.g. lag -1024 to 1024
private LinkedList<Double> mPeaksValue; //the value at mPeaksPos
private double[] mData;
/**
* Simple method for finding local maxima in an array
* @param data input
* @param width minimum distance between peaks
* @param threshold minimum value of peaks
* @param decayRate how quickly previous peaks are forgotten
* @param isRelative minimum value of peaks is relative to local average
* @return array of peaks
*/
public Peaks(double[] data, int width, double threshold, double decayRate, boolean isRelative){
this.mData = data;
this.mPeaksIdx = new LinkedList<>();
this.mPeaksPos = new LinkedList<>();
this.mPeaksValue = new LinkedList<>();
//create the peaks
simplePeakFinder(data, width, threshold, decayRate, isRelative);
updateLists();
}
public LinkedList<Integer> getPeaksIdx() {
return mPeaksIdx;
}
public int[] getPeaksIdxAsArray() {
return mPeaksIdx.stream().mapToInt(i->i).toArray();
}
public LinkedList<Integer> getPeaksPos() {
return mPeaksPos;
}
public double[] getPeaksPosAsArray() {
return mPeaksPos.stream().mapToDouble(i -> i).toArray();
}
public LinkedList<Double> getPeaksValue() {
return mPeaksValue;
}
public double[] getPeaksValueAsArray() {
return mPeaksValue.stream().mapToDouble(i -> i).toArray();
}
public double[] getData(){
return mData;
}
//TODO: implement findFalseDetectedPeaks
public void improveResults(double[] data){
updateLists();
}
public double[] getPeaksValueWithoutZeroIdx() {
double[] values = new double[mPeaksIdx.size() - 1];
int mid = (mData.length / 2);
int j = 0;
for(int i = 0; i < mPeaksIdx.size(); ++i){
if(!(mPeaksIdx.get(i) == mid)){
values[j] = mPeaksValue.get(i);
++j;
}
}
return values;
}
public double[] getPeaksValueWithoutNegativeValues() {
double[] values = new double[mPeaksIdx.size() - 1];
int i = 0;
for(Integer idx : mPeaksIdx) {
double curVal = mPeaksValue.get(idx);
if(curVal > 0){
values[i] = curVal;
++i;
}
}
return values;
}
public double[] getPeaksValueWithoutZeroIdxAndNegativeValues(){
double[] values = new double[mPeaksIdx.size() - 1];
int mid = (mData.length / 2);
int j = 0;
for(int i = 0; i < mPeaksIdx.size(); ++i){
double curVal = mPeaksValue.get(i);
if(!(mPeaksIdx.get(i) == mid) && curVal > 0){
values[j] = curVal;
++j;
}
}
return values;
}
public boolean hasPeaks() {
return mPeaksIdx.size() > 1;
}
/**
* Provides an estimation of beats per minute given a samplerate in milliseconds
* @param sampleRate_ms
* @return bpm if peaks found and conducting activity recognized, else -1
*/
public double getBPM(double sampleRate_ms){
//todo: rückweisungsklasse kann auch hier mit rein.
if(hasPeaks()){
//todo diff and mean method for linkedlists for speed
//return 60000 / Utils.mean(Utils.diff(mPeaksPos.stream().mapToDouble(i -> i * sampleRate_ms).toArray()));
return 60000 / (sampleRate_ms * Utils.mean(Utils.diff(mPeaksPos.stream().mapToDouble(i -> i).toArray())));
}
return -1;
}
/**
* updates the position and values of the found peaks.
* call this if peaks are somewhat changed.
*/
private void updateLists(){
//fill the position and the value lists
for(Integer idx : mPeaksIdx){
int mid = (mData.length / 2);
mPeaksPos.add(idx - mid);
mPeaksValue.add(mData[idx]);
}
}
//TODO: findPeaks method identical to Matlab... with PeakProminence
private void simplePeakFinder(double[] data, int width, double threshold, double decayRate, boolean isRelative) {
int maxp;
int mid = 0;
int end = data.length;
double av = data[0];
while (mid < end) {
av = decayRate * av + (1 - decayRate) * data[mid];
if (av < data[mid])
av = data[mid];
int i = mid - width;
if (i < 0)
i = 0;
int stop = mid + width + 1;
if (stop > data.length)
stop = data.length;
maxp = i;
for (i++; i < stop; i++)
if (data[i] > data[maxp])
maxp = i;
if (maxp == mid) {
if (overThreshold(data, maxp, width, threshold, isRelative, av)){
this.mPeaksIdx.add(new Integer(maxp));
}
}
mid++;
}
}
private boolean overThreshold(double[] data, int index, int width,
double threshold, boolean isRelative,
double av) {
int pre = 3;
int post = 1;
if (data[index] < av)
return false;
if (isRelative) {
int iStart = index - pre * width;
if (iStart < 0)
iStart = 0;
int iStop = index + post * width;
if (iStop > data.length)
iStop = data.length;
double sum = 0;
int count = iStop - iStart;
while (iStart < iStop)
sum += data[iStart++];
return (data[index] > sum / count + threshold);
} else
return (data[index] > threshold);
}
}

View File

@@ -0,0 +1,37 @@
package de.tonifetzer.conductorswatch.utilities;
import java.util.LinkedList;
/**
* Created by toni on 18/12/17.
*/
public class MovingFilter {
private int mSize;
private double mTotal = 0d;
private LinkedList<Double> mSamples = new LinkedList<>();
public MovingFilter(int size) {
this.mSize = size;
}
public void add(double x) {
mTotal += x;
mSamples.add(x);
if(mSamples.size() > mSize){
mTotal -= mSamples.remove();
}
}
public double getAverage() {
return mTotal / mSamples.size();
}
public double getMedian() {
return Utils.median(mSamples);
}
public void clear(){
mSamples.clear();
}
}

View File

@@ -0,0 +1,33 @@
package de.tonifetzer.conductorswatch.utilities;
/**
* Created by toni on 18/12/17.
*/
public class SimpleKalman {
private double mSigmaUpdate;
private double mSigmaPrediction;
private double mMu;
private double mSigma;
SimpleKalman(double initialMu, double initialSigma, double sigmaUpdate, double sigmaPrediction){
mSigmaUpdate = sigmaUpdate;
mSigmaPrediction = sigmaPrediction;
mMu = initialMu;
mSigma = initialSigma;
}
public double update(double data){
//prediction
mMu = mMu;
mSigma += mSigmaPrediction;
//update
double k = mSigma / (mSigma + mSigmaUpdate);
mMu = mMu + k * (data - mMu);
mSigma = (1 - k) * mSigma;
return mMu;
}
}