Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c187a3992 | ||
|
|
80e8fd499d | ||
|
|
691f3a684a | ||
|
|
72a672a80b | ||
|
|
a438813233 | ||
|
|
838242ba76 | ||
|
|
a9532f8129 | ||
|
|
49042a0cfb | ||
|
|
6bb8bb6b4f | ||
|
|
42e38bd929 | ||
|
|
b8f20ec8d9 |
6
android/ConductorsPhone/.idea/vcs.xml
generated
Normal file
6
android/ConductorsPhone/.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,8 +1,6 @@
|
||||
package de.tonifetzer.conductorswatch;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaPlayer;
|
||||
import android.media.SoundPool;
|
||||
|
||||
import java.util.TimerTask;
|
||||
@@ -10,7 +8,6 @@ import java.util.TimerTask;
|
||||
/**
|
||||
* Created by toni on 20/12/17.
|
||||
*/
|
||||
|
||||
public class Metronome extends TimerTask {
|
||||
|
||||
//private MediaPlayer mMediaPlayer;
|
||||
|
||||
@@ -7,7 +7,7 @@ buildscript {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.1.2'
|
||||
classpath 'com.android.tools.build:gradle:3.3.0'
|
||||
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#Fri Apr 27 11:02:05 CEST 2018
|
||||
#Tue Jan 29 16:48:07 CET 2019
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
|
||||
|
||||
@@ -51,7 +51,7 @@ public class MainActivity extends FragmentActivity implements PopupMenu.OnMenuIt
|
||||
//textview to show the bpm estimation
|
||||
mBpmTextView = (TextView) findViewById(R.id.bpmText);
|
||||
|
||||
mEstimator = new Estimator();
|
||||
mEstimator = new Estimator(this);
|
||||
mEstimator.addListener(this);
|
||||
|
||||
// ensures the connection to the bt sensor board
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.tonifetzer.conductorssensor.estimation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.LinkedList;
|
||||
@@ -9,6 +10,7 @@ import java.util.TimerTask;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
import de.tonifetzer.conductorssensor.sensor.SensorBoard;
|
||||
import de.tonifetzer.conductorssensor.utilities.SensorDataFileWriter;
|
||||
import de.tonifetzer.conductorssensor.utilities.Utils;
|
||||
|
||||
public class Estimator implements SensorBoard.OnSensorBoardDataListener {
|
||||
@@ -18,18 +20,25 @@ public class Estimator implements SensorBoard.OnSensorBoardDataListener {
|
||||
private BpmEstimator mBpmEstimator;
|
||||
|
||||
private Timer mTimer = new Timer();
|
||||
private SensorDataFileWriter mFileWriter;
|
||||
private Context mContext;
|
||||
|
||||
public Estimator(){
|
||||
public Estimator(Context context){
|
||||
|
||||
mAccelerometerWindowBuffer = new AccelerometerWindowBuffer(6000, 750);
|
||||
mBpmEstimator = new BpmEstimator(mAccelerometerWindowBuffer, 0, 5000);
|
||||
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
public void start(SensorBoard sensorBoard){
|
||||
if(sensorBoard != null){
|
||||
mSensorBoard = sensorBoard;
|
||||
mSensorBoard.addListener(this);
|
||||
mSensorBoard.startAccelerometer();
|
||||
//mSensorBoard.startAccelerometer();
|
||||
mSensorBoard.startLinearAccelerometer();
|
||||
|
||||
mFileWriter = new SensorDataFileWriter(mContext);
|
||||
|
||||
startWorker();
|
||||
|
||||
@@ -41,11 +50,14 @@ public class Estimator implements SensorBoard.OnSensorBoardDataListener {
|
||||
public void stop(){
|
||||
if(mSensorBoard != null){
|
||||
mSensorBoard.removeListener(this);
|
||||
mSensorBoard.stopAccelerometer();
|
||||
//mSensorBoard.stopAccelerometer();
|
||||
mSensorBoard.stopLinearAccelerometer();
|
||||
|
||||
mAccelerometerWindowBuffer.clear();
|
||||
mBpmEstimator.reset();
|
||||
|
||||
mFileWriter.toDisk();
|
||||
|
||||
} else {
|
||||
Log.i("Estimator","Cant stop estimator. SensorBoard is null.");
|
||||
}
|
||||
@@ -55,8 +67,11 @@ public class Estimator implements SensorBoard.OnSensorBoardDataListener {
|
||||
public void onAccelerometerChanged(AccelerometerData data) {
|
||||
mAccelerometerWindowBuffer.add(data);
|
||||
|
||||
//Log.d("Acc: ", "x: " + data.x + " y: " + data.y + " z: " + data.z);
|
||||
|
||||
//todo: save data into stream and write on disk
|
||||
|
||||
mFileWriter.writeVector3D(data.ts, 2, data.x, data.y, data.z);
|
||||
}
|
||||
|
||||
private void startWorker() {
|
||||
|
||||
@@ -9,8 +9,6 @@ import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.mbientlab.metawear.Data;
|
||||
@@ -22,6 +20,8 @@ import com.mbientlab.metawear.builder.RouteBuilder;
|
||||
import com.mbientlab.metawear.builder.RouteComponent;
|
||||
import com.mbientlab.metawear.data.Acceleration;
|
||||
import com.mbientlab.metawear.module.Accelerometer;
|
||||
import com.mbientlab.metawear.module.SensorFusionBosch;
|
||||
import com.mbientlab.metawear.module.SensorFusionBosch.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
@@ -36,7 +36,9 @@ public class SensorBoard implements ServiceConnection {
|
||||
private BtleService.LocalBinder serviceBinder;
|
||||
private MetaWearBoard mMetaBoard;
|
||||
private BluetoothDevice mBluetoothDevice;
|
||||
|
||||
private Accelerometer mAccelerometer;
|
||||
private SensorFusionBosch mSensorFusion;
|
||||
|
||||
private boolean mBoardConnected = false;
|
||||
|
||||
@@ -203,6 +205,7 @@ public class SensorBoard implements ServiceConnection {
|
||||
|
||||
mAccelerometer.configure()
|
||||
.odr(75f) // 75hz
|
||||
.range(4f)
|
||||
.commit();
|
||||
|
||||
mAccelerometer.acceleration().addRouteAsync(new RouteBuilder() {
|
||||
@@ -243,6 +246,51 @@ public class SensorBoard implements ServiceConnection {
|
||||
|
||||
}
|
||||
|
||||
public void startLinearAccelerometer(){
|
||||
|
||||
if(mMetaBoard.isConnected()){
|
||||
mSensorFusion = mMetaBoard.getModule(SensorFusionBosch.class);
|
||||
|
||||
mSensorFusion.configure()
|
||||
.mode(Mode.NDOF)
|
||||
.accRange(AccRange.AR_16G)
|
||||
.gyroRange(GyroRange.GR_2000DPS)
|
||||
.commit();
|
||||
|
||||
mSensorFusion.linearAcceleration().addRouteAsync(new RouteBuilder() {
|
||||
@Override
|
||||
public void configure(RouteComponent source) {
|
||||
source.stream(new Subscriber() {
|
||||
@Override
|
||||
public void apply(Data data, Object... env) {
|
||||
for(OnSensorBoardDataListener listener : mDataListeners){
|
||||
listener.onAccelerometerChanged(new AccelerometerData(
|
||||
System.currentTimeMillis(),
|
||||
data.value(Acceleration.class).x(),
|
||||
data.value(Acceleration.class).y(),
|
||||
data.value(Acceleration.class).z()));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}).continueWith(new Continuation<Route, Void>() {
|
||||
@Override
|
||||
public Void then(Task<Route> task) throws Exception {
|
||||
mSensorFusion.linearAcceleration().start();
|
||||
mSensorFusion.start();
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void stopLinearAccelerometer(){
|
||||
|
||||
if(mMetaBoard.isConnected()){
|
||||
mSensorFusion.stop();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Interface for callback onConnectionInfos
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package de.tonifetzer.conductorssensor.utilities;
|
||||
|
||||
/**
|
||||
* Created by toni on 12/01/18.
|
||||
*/
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Environment;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Class: DataFolder
|
||||
* Description: SDK save file class. Is able to open a folder on the device independent of the given
|
||||
* device and android skd version.
|
||||
*/
|
||||
public class DataFolder {
|
||||
|
||||
private File folder;
|
||||
private static final String TAG = "DataFolder";
|
||||
|
||||
public DataFolder(Context context, String folderName){
|
||||
|
||||
// 1) try external data folder
|
||||
folder = new File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), folderName);
|
||||
if (isOK(folder)) {return;}
|
||||
|
||||
// 2) try sd-card folder
|
||||
folder = new File(Environment.getExternalStorageDirectory() + "/" + folderName);
|
||||
if (isOK(folder)) {return;}
|
||||
|
||||
// 3) try internal data folder
|
||||
folder = new File(context.getApplicationInfo().dataDir);
|
||||
if (isOK(folder)) {return;}
|
||||
|
||||
// all failed
|
||||
throw new RuntimeException("failed to create/access storage folder");
|
||||
|
||||
}
|
||||
|
||||
/** ensure the given folder is OK */
|
||||
private static final boolean isOK(final File folder) {
|
||||
folder.mkdirs();
|
||||
final boolean ok = folder.exists() && folder.isDirectory();
|
||||
if (ok) {
|
||||
Log.d(TAG, "using: " + folder);
|
||||
} else {
|
||||
Log.d(TAG, "not OK: " + folder);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
public File getFolder(){
|
||||
return folder;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
package de.tonifetzer.conductorssensor.utilities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.util.Log;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import de.tonifetzer.conductorssensor.R;
|
||||
|
||||
/**
|
||||
* Created by toni on 14/01/18.
|
||||
*/
|
||||
|
||||
public class SensorDataFileWriter {
|
||||
|
||||
private static final int FLUSH_LIMIT = 2*1024*1024;
|
||||
private static final String TAG = "SensorDataFileWriter";
|
||||
|
||||
private File mSensorDataFile;
|
||||
private FileOutputStream mFileOutputStream;
|
||||
private StringBuilder mStringBuilder = new StringBuilder();
|
||||
private final DataFolder mFolder;
|
||||
private boolean mStreamOpenend = false;
|
||||
|
||||
public SensorDataFileWriter(Context context) {
|
||||
|
||||
mFolder = new DataFolder(context, "sensorOutFiles");
|
||||
|
||||
//write some description for this file in the first line
|
||||
EditText editView = ((Activity) context).findViewById(R.id.comments);
|
||||
TextView textView = ((Activity) context).findViewById(R.id.bpmText);
|
||||
writeDescription("Kommentar: " + editView.getText().toString() + "\nMetronom: " + textView.getText().toString() + " bpm");
|
||||
|
||||
//open connection to file
|
||||
open();
|
||||
}
|
||||
|
||||
public final void writeVector3D(long ts, int id, double x, double y, double z){
|
||||
|
||||
synchronized (this) {
|
||||
|
||||
if (mStreamOpenend) {
|
||||
|
||||
mStringBuilder.append(ts).append(';').append(id).append(';').append(x).append(';').append(y).append(';').append(z).append('\n');
|
||||
|
||||
if (mStringBuilder.length() > FLUSH_LIMIT) {flush(false);}
|
||||
} else {
|
||||
open();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void writeDescription(String str){
|
||||
synchronized (this) {
|
||||
mStringBuilder.append(str).append('\n').append('\n');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// flush data to disk
|
||||
public final void toDisk(){
|
||||
synchronized (this) {
|
||||
flush(true);
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
/** helper method for exception-less writing. DO NOT CALL DIRECTLY! */
|
||||
private void _write(final byte[] data) {
|
||||
try {
|
||||
mFileOutputStream.write(data);
|
||||
Log.d(TAG, "flushed " + data.length + " bytes to disk");
|
||||
} catch (final Exception e) {
|
||||
throw new RuntimeException("error while writing log-file", e);
|
||||
}
|
||||
}
|
||||
|
||||
/** helper-class for background writing */
|
||||
class FlushAsync extends AsyncTask<byte[], Integer, Integer> {
|
||||
@Override
|
||||
protected final Integer doInBackground(byte[][] data) {
|
||||
_write(data[0]);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/** flush current buffer-contents to disk */
|
||||
private void flush(boolean sync) {
|
||||
|
||||
// fetch current buffer contents to write and hereafter empty the buffer
|
||||
// this action MUST be atomic, just like the add-method
|
||||
byte[] data = null;
|
||||
synchronized (this) {
|
||||
data = mStringBuilder.toString().getBytes(); // fetch data to write
|
||||
mStringBuilder.setLength(0); // reset the buffer
|
||||
}
|
||||
|
||||
// write
|
||||
if (sync) {
|
||||
// write to disk using the current thread
|
||||
_write(data);
|
||||
} else {
|
||||
// write to disk using a background-thread
|
||||
new FlushAsync().execute(new byte[][] {data});
|
||||
}
|
||||
}
|
||||
|
||||
private void open() {
|
||||
mSensorDataFile = new File(mFolder.getFolder(), System.currentTimeMillis() + ".csv");
|
||||
|
||||
try {
|
||||
mFileOutputStream = new FileOutputStream(mSensorDataFile);
|
||||
mStreamOpenend = true;
|
||||
|
||||
Log.d(TAG, "will write to: " + mSensorDataFile.toString());
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
|
||||
mStreamOpenend = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void close() {
|
||||
try {
|
||||
mFileOutputStream.close();
|
||||
mStreamOpenend = false;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,5 +47,5 @@ dependencies {
|
||||
implementation 'com.android.support:recyclerview-v7:26.1.0'
|
||||
implementation 'com.android.support:wear:26.1.0'
|
||||
compileOnly 'com.google.android.wearable:wearable:2.2.0'
|
||||
compile 'com.github.wendykierp:JTransforms:3.1'
|
||||
api 'com.github.wendykierp:JTransforms:3.1'
|
||||
}
|
||||
|
||||
@@ -8,20 +8,23 @@ import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Vibrator;
|
||||
import android.support.wearable.activity.WearableActivity;
|
||||
import android.util.Log;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import de.tonifetzer.conductorswatch.network.SensorDataFileSender;
|
||||
import de.tonifetzer.conductorswatch.ui.Croller;
|
||||
import de.tonifetzer.conductorswatch.ui.TapBpm;
|
||||
import de.tonifetzer.conductorswatch.utilities.Utils;
|
||||
|
||||
public class MainActivity extends WearableActivity implements WorkerFragment.OnFragmentInteractionListener, TapBpm.OnTapBpmListener {
|
||||
public class MainActivity extends WearableActivity implements WorkerFragment.OnFragmentInteractionListener, TapBpm.OnTapBpmListener, Croller.onProgressChangedListener {
|
||||
|
||||
// member
|
||||
private TextView mTextView;
|
||||
private Croller mCroller;
|
||||
private GestureDetector mDetector;
|
||||
private boolean mModeRecord = false;
|
||||
|
||||
// connection to phone stuff
|
||||
@@ -32,40 +35,39 @@ public class MainActivity extends WearableActivity implements WorkerFragment.OnF
|
||||
private volatile boolean mReadyToSend = true;
|
||||
|
||||
// display center
|
||||
private int mDisplayWidth;
|
||||
private int mDisplayHeight;
|
||||
private Point mDisplayCenter;
|
||||
|
||||
// saved Bpm to reset after recording
|
||||
private int mMetronomBpm = 80;
|
||||
private int mLastSendBPM = 80;
|
||||
|
||||
//threading
|
||||
final ExecutorService mExecutorService = Executors.newCachedThreadPool();
|
||||
|
||||
// tapping
|
||||
private Thread mTapBpmThread;
|
||||
private TapBpm mTapBpm;
|
||||
private boolean mTapRecognized = false;
|
||||
private boolean mTappingStarted = false;
|
||||
private int mTapBpmEstimation;
|
||||
private Vibrator mVibrator;
|
||||
|
||||
//parameter for long press to start the worker
|
||||
private Point mPreviousMovePoint;
|
||||
private int mDistanceJitteringLongPress = 50; // in pixel
|
||||
private int mLongPressDelay = 1200; // in Milliseconds
|
||||
private boolean mLongPressHandlerActivated = false;
|
||||
private final Handler mHandler = new Handler();
|
||||
|
||||
// runnable to switch between record mode and normal mode
|
||||
private Runnable mLongPressed = new Runnable() {
|
||||
public void run() {
|
||||
Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
|
||||
vibrator.vibrate(100);
|
||||
|
||||
mLongPressHandlerActivated = true;
|
||||
|
||||
mModeRecord = !mModeRecord;
|
||||
|
||||
if (mModeRecord) {
|
||||
WorkerFragment worker = new WorkerFragment();
|
||||
|
||||
//if we tapped, finish tapping
|
||||
onTapFinished();
|
||||
|
||||
//provide the fragment with the bpm set
|
||||
Bundle args = new Bundle();
|
||||
args.putInt("bpm", mCroller.getProgress());
|
||||
@@ -86,6 +88,7 @@ public class MainActivity extends WearableActivity implements WorkerFragment.OnF
|
||||
transaction.commit();
|
||||
} else {
|
||||
|
||||
//reset colors from red to green
|
||||
mCroller.setProgressPrimaryColor(Color.parseColor("#158b69"));
|
||||
mCroller.setBackCircleColor(Color.parseColor("#158b69"));
|
||||
mTextView.setTextColor(Color.parseColor("#158b69"));
|
||||
@@ -100,6 +103,7 @@ public class MainActivity extends WearableActivity implements WorkerFragment.OnF
|
||||
}
|
||||
};
|
||||
|
||||
//if we press the center button long enough, start mode switching runnable
|
||||
private boolean onLongPressCustomized(MotionEvent ev) {
|
||||
|
||||
Point currentPoint = new Point((int) ev.getX(), (int) ev.getY());
|
||||
@@ -115,27 +119,11 @@ public class MainActivity extends WearableActivity implements WorkerFragment.OnF
|
||||
mHandler.postDelayed(mLongPressed, mLongPressDelay);
|
||||
}
|
||||
|
||||
if ((ev.getAction() == MotionEvent.ACTION_MOVE) || (ev.getAction() == MotionEvent.ACTION_HOVER_MOVE)) {
|
||||
|
||||
/*
|
||||
if (mPreviousMovePoint == null) {
|
||||
mPreviousMovePoint = currentPoint;
|
||||
} else {
|
||||
int dx = Math.abs(currentPoint.x - mPreviousMovePoint.x);
|
||||
int dy = Math.abs(currentPoint.y - mPreviousMovePoint.y);
|
||||
int distance = (int) Math.sqrt(dx * dx + dy * dy);
|
||||
if (distance > mDistanceJitteringLongPress) {
|
||||
mHandler.removeCallbacks(mLongPressed);
|
||||
return false;
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
if (ev.getAction() == MotionEvent.ACTION_UP) {
|
||||
mHandler.removeCallbacks(mLongPressed);
|
||||
if (mLongPressHandlerActivated) {
|
||||
mLongPressHandlerActivated = false;
|
||||
mPreviousMovePoint = null;
|
||||
//mPreviousMovePoint = null;
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
@@ -144,7 +132,9 @@ public class MainActivity extends WearableActivity implements WorkerFragment.OnF
|
||||
return false;
|
||||
}
|
||||
|
||||
//animation that fills the circle slowly with color
|
||||
private boolean onColorChanging(MotionEvent ev, int MainColor) {
|
||||
//TODO: könnte man komplett in den Croller auslagern.
|
||||
Point currentPoint = new Point((int) ev.getX(), (int) ev.getY());
|
||||
|
||||
//only works within the maincircle of the scroller
|
||||
@@ -181,6 +171,7 @@ public class MainActivity extends WearableActivity implements WorkerFragment.OnF
|
||||
return false;
|
||||
}
|
||||
|
||||
//starts a runnable that estimates the bpm (for metronom) by tapping on the center circle
|
||||
public boolean onTapForBpm(MotionEvent ev) {
|
||||
|
||||
Point currentPoint = new Point((int) ev.getX(), (int) ev.getY());
|
||||
@@ -194,22 +185,16 @@ public class MainActivity extends WearableActivity implements WorkerFragment.OnF
|
||||
}
|
||||
|
||||
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
|
||||
if (!mTapRecognized) {
|
||||
|
||||
mTapBpm.addTimestamp(System.currentTimeMillis());
|
||||
mTapRecognized = true;
|
||||
}
|
||||
mTapBpm.addTimestamp(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
if (ev.getAction() == MotionEvent.ACTION_UP) {
|
||||
|
||||
if (!mTapBpmThread.isAlive() && mTapRecognized) {
|
||||
mTapBpmThread.start();
|
||||
if (!mTappingStarted) {
|
||||
mTappingStarted = true;
|
||||
mCroller.setLabel("Tippe weiter");
|
||||
mExecutorService.submit(mTapBpm);
|
||||
}
|
||||
mTapRecognized = false;
|
||||
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -221,57 +206,20 @@ public class MainActivity extends WearableActivity implements WorkerFragment.OnF
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
//get display infos
|
||||
mDisplayWidth = this.getResources().getDisplayMetrics().widthPixels;
|
||||
mDisplayHeight = this.getResources().getDisplayMetrics().heightPixels;
|
||||
mDisplayCenter = new Point((mDisplayWidth / 2), (mDisplayHeight / 2));
|
||||
int displayWidth = this.getResources().getDisplayMetrics().widthPixels;
|
||||
int displayHeight = this.getResources().getDisplayMetrics().heightPixels;
|
||||
mDisplayCenter = new Point((displayWidth / 2), (displayHeight / 2));
|
||||
|
||||
mTapBpm = new TapBpm();
|
||||
mTapBpm.add(this);
|
||||
mTapBpmThread = new Thread(mTapBpm, "tapThread");
|
||||
//ui and motion stuff
|
||||
mVibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
|
||||
mTapBpm = new TapBpm();
|
||||
mTextView = findViewById(R.id.bpmText);
|
||||
mCroller = findViewById(R.id.croller);
|
||||
|
||||
mTextView = (TextView) findViewById(R.id.bpmText);
|
||||
|
||||
//listener
|
||||
mCroller.setOnProgressChangedListener(this);
|
||||
mSender = new SensorDataFileSender(this);
|
||||
|
||||
// circular progress bar
|
||||
mCroller = (Croller) findViewById(R.id.croller);
|
||||
mCroller.setOnProgressChangedListener(new Croller.onProgressChangedListener() {
|
||||
@Override
|
||||
public void onProgressChanged(int currentBPM) {
|
||||
|
||||
if(currentBPM != mLastSendBPM){
|
||||
|
||||
mTextView.setText(Integer.toString(currentBPM));
|
||||
mSender.sendMessage(UPDATE_PATH + ":" + mMetronomBpm + ":" + currentBPM);
|
||||
mLastSendBPM = currentBPM;
|
||||
}
|
||||
|
||||
if(mReadyToSend){
|
||||
mReadyToSend = false;
|
||||
new java.util.Timer().schedule(
|
||||
new java.util.TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
// your code here
|
||||
|
||||
mReadyToSend = true;
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// detector for double clicks
|
||||
mDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {
|
||||
|
||||
public boolean onDoubleTap(MotionEvent ev) {
|
||||
Log.d("Gesture", "onDoubleTap: " + ev.toString());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
});
|
||||
mTapBpm.add(this);
|
||||
|
||||
// Enables Always-on
|
||||
setAmbientEnabled();
|
||||
@@ -336,14 +284,11 @@ public class MainActivity extends WearableActivity implements WorkerFragment.OnF
|
||||
mTapBpmEstimation = bpm;
|
||||
|
||||
synchronized (this) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (mTapBpmEstimation > 0) {
|
||||
mCroller.setProgress(mTapBpmEstimation);
|
||||
mCroller.setLabel("Fertig");
|
||||
mVibrator.vibrate(10);
|
||||
}
|
||||
runOnUiThread(() -> {
|
||||
if (mTapBpmEstimation > 0) {
|
||||
mCroller.setProgress(mTapBpmEstimation);
|
||||
mCroller.setLabel("Fertig");
|
||||
mVibrator.vibrate(20);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -352,23 +297,35 @@ public class MainActivity extends WearableActivity implements WorkerFragment.OnF
|
||||
@Override
|
||||
public void onTapFinished() {
|
||||
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
synchronized (this) {
|
||||
runOnUiThread(() -> {
|
||||
mCroller.setLabel("");
|
||||
}
|
||||
});
|
||||
|
||||
mTapBpm.clearTimestamps();
|
||||
mTapRecognized = false;
|
||||
/*
|
||||
mTapBpmThread.interrupt();
|
||||
try {
|
||||
mTapBpmThread.join();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
mTapBpm.clearTimestamps();
|
||||
mTappingStarted = false;
|
||||
});
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(int currentBPM) {
|
||||
|
||||
if(currentBPM != mLastSendBPM){
|
||||
mTextView.setText(String.format(Locale.getDefault(), "%d", currentBPM));
|
||||
mSender.sendMessage(UPDATE_PATH + ":" + mMetronomBpm + ":" + currentBPM);
|
||||
mLastSendBPM = currentBPM;
|
||||
}
|
||||
|
||||
//künstlicher delay, da es sonst so häufig aufgerufen wird und das sendMessage dann in die Hose geht.
|
||||
if(mReadyToSend){
|
||||
mReadyToSend = false;
|
||||
new java.util.Timer().schedule(
|
||||
new java.util.TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
mReadyToSend = true;
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
package de.tonifetzer.conductorswatch;
|
||||
|
||||
import android.os.Vibrator;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* Created by toni on 13/11/17.
|
||||
*/
|
||||
|
||||
//TODO: implement the metronome similar to phone. since thread sleeping is no accurate enough
|
||||
public class Metronome implements Runnable{
|
||||
|
||||
private volatile boolean mRunning = true;
|
||||
private int mBPM;
|
||||
|
||||
public Metronome(int bpm){
|
||||
mBPM = bpm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
while(mRunning){
|
||||
|
||||
for (OnMetronomeListener listener:listeners) {
|
||||
listener.onNewClick();
|
||||
}
|
||||
|
||||
try {
|
||||
if(mBPM > 0){
|
||||
Thread.sleep(60000 / mBPM);
|
||||
} else {
|
||||
Thread.sleep(60000);
|
||||
}
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
mRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for callback metronome clicks
|
||||
* I know that java has observable.. but this why it is more clean and easy
|
||||
*/
|
||||
public interface OnMetronomeListener {
|
||||
void onNewClick();
|
||||
}
|
||||
|
||||
private List<OnMetronomeListener> listeners = new CopyOnWriteArrayList<OnMetronomeListener>();
|
||||
public void add(OnMetronomeListener listener){listeners.add(listener);}
|
||||
public void remove(OnMetronomeListener listener){listeners.remove(listener);}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package de.tonifetzer.conductorswatch;
|
||||
|
||||
public interface OnCrollerChangeListener {
|
||||
void onProgressChanged(Croller croller, int progress);
|
||||
|
||||
void onStartTrackingTouch(Croller croller);
|
||||
|
||||
void onStopTrackingTouch(Croller croller);
|
||||
}
|
||||
@@ -4,14 +4,19 @@ import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.app.Fragment;
|
||||
import android.os.Vibrator;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.Timer;
|
||||
import java.util.Vector;
|
||||
|
||||
import de.tonifetzer.conductorswatch.estimation.Estimator;
|
||||
import de.tonifetzer.conductorswatch.ui.Croller;
|
||||
import de.tonifetzer.conductorswatch.ui.Metronome;
|
||||
|
||||
|
||||
/**
|
||||
* A simple {@link Fragment} subclass.
|
||||
@@ -23,14 +28,11 @@ public class WorkerFragment extends Fragment implements Metronome.OnMetronomeLis
|
||||
|
||||
private OnFragmentInteractionListener mListener;
|
||||
private Vector<Double> mBpmList;
|
||||
private byte[] mSensorData;
|
||||
private boolean mWorkerRunning = false;
|
||||
|
||||
private Estimator mEstimator;
|
||||
private Metronome mMetronome;
|
||||
private Thread mMetronomeThread;
|
||||
private Timer mTimer;
|
||||
|
||||
private Vibrator mVibrator;
|
||||
private TextView mTextView;
|
||||
private Croller mCroller;
|
||||
|
||||
@@ -61,11 +63,7 @@ public class WorkerFragment extends Fragment implements Metronome.OnMetronomeLis
|
||||
mBpmList = new Vector<Double>();
|
||||
|
||||
// init metronome and listener
|
||||
mMetronome = new Metronome(getArguments().getInt("bpm"));
|
||||
mMetronome.add(this);
|
||||
mMetronomeThread = new Thread(mMetronome, "metronomThread");
|
||||
|
||||
mVibrator = (Vibrator) this.getActivity().getSystemService(Context.VIBRATOR_SERVICE);
|
||||
mTimer = new Timer();
|
||||
|
||||
//keep screen always on
|
||||
this.getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
@@ -96,8 +94,8 @@ public class WorkerFragment extends Fragment implements Metronome.OnMetronomeLis
|
||||
// start the worker thread for bpm estimator
|
||||
mEstimator.start();
|
||||
|
||||
// start the worker thread for metronom
|
||||
mMetronomeThread.start();
|
||||
// start the timer for metronom based on the predefined bpm
|
||||
mTimer.scheduleAtFixedRate(new Metronome(getContext()), 0, 60000 / getArguments().getInt("bpm"));
|
||||
|
||||
// everything is running
|
||||
mWorkerRunning = true;
|
||||
@@ -113,7 +111,7 @@ public class WorkerFragment extends Fragment implements Metronome.OnMetronomeLis
|
||||
mEstimator.stop();
|
||||
|
||||
// stop the worker thread for metronom
|
||||
mMetronome.stop();
|
||||
mTimer.cancel();
|
||||
|
||||
//private listener with list of all estimated bpm
|
||||
if (mListener != null) {
|
||||
@@ -143,7 +141,6 @@ public class WorkerFragment extends Fragment implements Metronome.OnMetronomeLis
|
||||
@Override
|
||||
public void onNewDataAvailable(double bpm) {
|
||||
|
||||
//TODO: what if multiple threads access mBpmList? put into synchronized? @frank fragen :D
|
||||
if(mWorkerRunning){
|
||||
|
||||
mBpmList.add(bpm);
|
||||
@@ -154,16 +151,11 @@ public class WorkerFragment extends Fragment implements Metronome.OnMetronomeLis
|
||||
|
||||
// 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?
|
||||
|
||||
if(getActivity() != null) {
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
int tmpBPM = (int) (Math.round(mBpmList.lastElement()));
|
||||
mTextView.setText(String.valueOf(tmpBPM));
|
||||
mCroller.setProgress(tmpBPM);
|
||||
}
|
||||
getActivity().runOnUiThread(() -> {
|
||||
int tmpBPM = (int) (Math.round(mBpmList.lastElement()));
|
||||
mTextView.setText(String.valueOf(tmpBPM));
|
||||
mCroller.setProgress(tmpBPM);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,19 @@
|
||||
package de.tonifetzer.conductorswatch;
|
||||
package de.tonifetzer.conductorswatch.estimation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.hardware.Sensor;
|
||||
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;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import de.tonifetzer.conductorswatch.bpmEstimation.AccelerometerData;
|
||||
import de.tonifetzer.conductorswatch.bpmEstimation.AccelerometerWindowBuffer;
|
||||
import de.tonifetzer.conductorswatch.bpmEstimation.BpmEstimator;
|
||||
import de.tonifetzer.conductorswatch.network.SensorDataFileSender;
|
||||
import de.tonifetzer.conductorswatch.estimation.accelerometer.AccelerometerData;
|
||||
import de.tonifetzer.conductorswatch.estimation.accelerometer.AccelerometerWindowBuffer;
|
||||
import de.tonifetzer.conductorswatch.network.SensorDataFileStreamer;
|
||||
import de.tonifetzer.conductorswatch.utilities.ByteStreamWriter;
|
||||
import de.tonifetzer.conductorswatch.utilities.Utils;
|
||||
@@ -38,7 +29,7 @@ public class Estimator implements SensorEventListener {
|
||||
private Sensor mRawAccelerometer;
|
||||
private Context mContext;
|
||||
private AccelerometerWindowBuffer mAccelerometerWindowBuffer;
|
||||
private BpmEstimator mBpmEstimator;
|
||||
private EstimatorAutoCorr mBpmEstimator;
|
||||
private double mCurrentBpm;
|
||||
|
||||
private ByteStreamWriter mByteStreamWriterAcc;
|
||||
@@ -63,7 +54,7 @@ public class Estimator implements SensorEventListener {
|
||||
|
||||
|
||||
mAccelerometerWindowBuffer = new AccelerometerWindowBuffer(6000, 750);
|
||||
mBpmEstimator = new BpmEstimator(mAccelerometerWindowBuffer, 0, 5000);
|
||||
mBpmEstimator = new EstimatorAutoCorr(mAccelerometerWindowBuffer, 0, 5000);
|
||||
|
||||
mTimer.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
@@ -1,17 +1,18 @@
|
||||
package de.tonifetzer.conductorswatch.bpmEstimation;
|
||||
package de.tonifetzer.conductorswatch.estimation;
|
||||
|
||||
import de.tonifetzer.conductorswatch.estimation.accelerometer.AccelerometerWindowBuffer;
|
||||
import de.tonifetzer.conductorswatch.estimation.accelerometer.AccelerometerInterpolator;
|
||||
import de.tonifetzer.conductorswatch.estimation.dsp.AutoCorrelation;
|
||||
import de.tonifetzer.conductorswatch.estimation.dsp.PeakDetector;
|
||||
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 {
|
||||
public class EstimatorAutoCorr {
|
||||
|
||||
private AccelerometerWindowBuffer mBuffer;
|
||||
private double mSampleRate_ms;
|
||||
@@ -28,7 +29,7 @@ public class BpmEstimator {
|
||||
//private SimpleKalman mKalman;
|
||||
|
||||
|
||||
public BpmEstimator(AccelerometerWindowBuffer windowBuffer, double sampleRate_ms, int resetAfter_ms){
|
||||
EstimatorAutoCorr(AccelerometerWindowBuffer windowBuffer, double sampleRate_ms, int resetAfter_ms){
|
||||
mBuffer = windowBuffer;
|
||||
mSampleRate_ms = sampleRate_ms;
|
||||
|
||||
@@ -47,8 +48,7 @@ public class BpmEstimator {
|
||||
//mKalman = new SimpleKalman();
|
||||
}
|
||||
|
||||
//TODO: we use the buffer from outside.. this buffer is continuously updated.. not good!
|
||||
public double estimate(AccelerometerWindowBuffer fixedWindow){
|
||||
double estimate(AccelerometerWindowBuffer fixedWindow){
|
||||
|
||||
double sampleRate = mSampleRate_ms;
|
||||
if(sampleRate <= 0){
|
||||
@@ -59,6 +59,8 @@ public class BpmEstimator {
|
||||
int breakhere = 0;
|
||||
}
|
||||
|
||||
//TODO: ist doch käse das zu interpolieren, das mach ich ja für jedes mal estimation.
|
||||
//TODO: eigentlich muss ich ja nur das größte interpolieren und dann supwindows nehmen.
|
||||
AccelerometerInterpolator interp = new AccelerometerInterpolator(fixedWindow, sampleRate);
|
||||
|
||||
//are we conducting?
|
||||
@@ -72,9 +74,9 @@ public class BpmEstimator {
|
||||
|
||||
//find a peak within range of 250 ms
|
||||
int peakWidth = (int) Math.round(250 / sampleRate);
|
||||
Peaks pX = new Peaks(xAutoCorr, peakWidth, 0.1f, 0, false);
|
||||
Peaks pY = new Peaks(yAutoCorr, peakWidth, 0.1f, 0, false);
|
||||
Peaks pZ = new Peaks(zAutoCorr, peakWidth, 0.1f, 0, false);
|
||||
PeakDetector pX = new PeakDetector(xAutoCorr, peakWidth, 0.1f, 0, false);
|
||||
PeakDetector pY = new PeakDetector(yAutoCorr, peakWidth, 0.1f, 0, false);
|
||||
PeakDetector pZ = new PeakDetector(zAutoCorr, peakWidth, 0.1f, 0, false);
|
||||
|
||||
mBpmHistory_X.add(pX.getBPM(sampleRate));
|
||||
mBpmHistory_Y.add(pY.getBPM(sampleRate));
|
||||
@@ -100,7 +102,7 @@ public class BpmEstimator {
|
||||
//mResetCounter = 0;
|
||||
}
|
||||
else {
|
||||
int resetAfter = (int) Math.round(mResetLimit_ms / (mBuffer.getOverlapSize()));
|
||||
int resetAfter = (int) Math.round((double) (mResetLimit_ms / (mBuffer.getOverlapSize())));
|
||||
if(++mResetCounter > resetAfter){
|
||||
mBpmHistory.clear();
|
||||
|
||||
@@ -124,7 +126,7 @@ public class BpmEstimator {
|
||||
return Utils.median(mBpmHistory);
|
||||
}
|
||||
|
||||
private double getBestBpmEstimation(Peaks peaksX, Peaks peaksY, Peaks peaksZ) throws IllegalArgumentException {
|
||||
private double getBestBpmEstimation(PeakDetector peaksX, PeakDetector peaksY, PeakDetector peaksZ) throws IllegalArgumentException {
|
||||
|
||||
int cntNumAxis = 0;
|
||||
double sumCorr = 0; //to prevent division by zero
|
||||
@@ -172,7 +174,7 @@ public class BpmEstimator {
|
||||
|
||||
//no peaks, reject
|
||||
if(cntNumAxis == 0){
|
||||
//throw new IllegalArgumentException("All Peaks are empty! -> Reject Estimation");
|
||||
//throw new IllegalArgumentException("All PeakDetector are empty! -> Reject Estimation");
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package de.tonifetzer.conductorswatch.estimation;
|
||||
|
||||
import java.util.LinkedList;
|
||||
|
||||
import de.tonifetzer.conductorswatch.estimation.accelerometer.AccelerometerWindowBuffer;
|
||||
import de.tonifetzer.conductorswatch.utilities.MovingFilter;
|
||||
|
||||
public class EstimatorDistCorr {
|
||||
|
||||
private AccelerometerWindowBuffer mBuffer;
|
||||
private double mSampleRate_ms;
|
||||
private LinkedList<Double> mBpmHistory;
|
||||
private int mResetCounter;
|
||||
private int mResetLimit_ms;
|
||||
|
||||
private MovingFilter mMvg;
|
||||
|
||||
EstimatorDistCorr(AccelerometerWindowBuffer windowBuffer, double sampleRate_ms, int resetAfter_ms){
|
||||
|
||||
mBuffer = windowBuffer;
|
||||
mSampleRate_ms = sampleRate_ms;
|
||||
mBpmHistory = new LinkedList<>();
|
||||
mResetCounter = 0;
|
||||
mResetLimit_ms = resetAfter_ms;
|
||||
|
||||
mMvg = new MovingFilter(2);
|
||||
}
|
||||
|
||||
//hier gehts weiter :)
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package de.tonifetzer.conductorswatch.bpmEstimation;
|
||||
package de.tonifetzer.conductorswatch.estimation.accelerometer;
|
||||
|
||||
/**
|
||||
* Created by toni on 15/12/17.
|
||||
@@ -1,7 +1,12 @@
|
||||
package de.tonifetzer.conductorswatch.bpmEstimation;
|
||||
package de.tonifetzer.conductorswatch.estimation.accelerometer;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import de.tonifetzer.conductorswatch.estimation.accelerometer.AccelerometerData;
|
||||
import de.tonifetzer.conductorswatch.estimation.accelerometer.AccelerometerWindow;
|
||||
import de.tonifetzer.conductorswatch.estimation.accelerometer.AccelerometerWindowBuffer;
|
||||
import de.tonifetzer.conductorswatch.utilities.Utils;
|
||||
|
||||
/**
|
||||
* Created by toni on 16/12/17.
|
||||
*/
|
||||
@@ -11,11 +16,12 @@ public class AccelerometerInterpolator {
|
||||
private double[] mY;
|
||||
private double[] mZ;
|
||||
private long[] mTsInterp;
|
||||
private int mSize;
|
||||
|
||||
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];
|
||||
mSize = (int) ((ab.getYongest().ts - (ab.getOldest().ts - (long) sampleRate_ms)) / (long) sampleRate_ms);
|
||||
mTsInterp = new long[mSize];
|
||||
int j = 0;
|
||||
for(long i = ab.getOldest().ts; i <= ab.getYongest().ts; i += sampleRate_ms){
|
||||
mTsInterp[j++] = i;
|
||||
@@ -30,6 +36,18 @@ public class AccelerometerInterpolator {
|
||||
return mTsInterp;
|
||||
}
|
||||
|
||||
public double getX(int idx){
|
||||
return mX[idx];
|
||||
}
|
||||
|
||||
public double getY(int idx){
|
||||
return mY[idx];
|
||||
}
|
||||
|
||||
public double getZ(int idx){
|
||||
return mZ[idx];
|
||||
}
|
||||
|
||||
public double[] getX(){
|
||||
return mX;
|
||||
}
|
||||
@@ -42,6 +60,10 @@ public class AccelerometerInterpolator {
|
||||
return mZ;
|
||||
}
|
||||
|
||||
public int size(){
|
||||
return mSize;
|
||||
}
|
||||
|
||||
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");
|
||||
@@ -104,5 +126,34 @@ public class AccelerometerInterpolator {
|
||||
return interpLinear(xd, y, xid);
|
||||
}
|
||||
|
||||
public static AccelerometerWindow interpolate(AccelerometerWindow data){
|
||||
|
||||
//calculate samplerate
|
||||
double[] rawTimestamps = data.getTs_D();
|
||||
double sampleRate_ms = Math.round(Utils.mean(Utils.diff(rawTimestamps)));
|
||||
|
||||
//create interpolated timestamps
|
||||
double[] interpolatedTimestamps;
|
||||
int size = (int) ((data.getLast().ts - (data.getFirst().ts - (long) sampleRate_ms)) / (long) sampleRate_ms);
|
||||
interpolatedTimestamps = new double[size];
|
||||
|
||||
int j = 0;
|
||||
for(double i = data.getFirst().ts; i <= data.getLast().ts; i += sampleRate_ms){
|
||||
interpolatedTimestamps[j++] = i;
|
||||
}
|
||||
|
||||
//interpolate the single axis separately
|
||||
double[] interpolatedX = interpLinear(rawTimestamps, data.getX(), interpolatedTimestamps);
|
||||
double[] interpolatedY = interpLinear(rawTimestamps, data.getY(), interpolatedTimestamps);
|
||||
double[] interpolatedZ = interpLinear(rawTimestamps, data.getZ(), interpolatedTimestamps);
|
||||
|
||||
//merge everything back into an ArrayList
|
||||
AccelerometerWindow interpolatedData = new AccelerometerWindow(size);
|
||||
for(int i = 0; i < size; ++i){
|
||||
interpolatedData.add(new AccelerometerData((long) rawTimestamps[i], interpolatedX[i], interpolatedY[i], interpolatedZ[i]));
|
||||
}
|
||||
|
||||
return interpolatedData;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package de.tonifetzer.conductorswatch.estimation.accelerometer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
|
||||
public class AccelerometerWindow extends ArrayList<AccelerometerData> {
|
||||
|
||||
public AccelerometerWindow(int size){
|
||||
super(size);
|
||||
}
|
||||
|
||||
public AccelerometerWindow(AccelerometerWindow other){
|
||||
super(other);
|
||||
}
|
||||
|
||||
public AccelerometerWindow(LinkedList<AccelerometerData> other){
|
||||
super(other);
|
||||
}
|
||||
|
||||
public AccelerometerData getLast() {
|
||||
//TODO: check if list is empty! this causes indexoutofbounce
|
||||
synchronized (this){
|
||||
return super.get(size() - 1);
|
||||
}
|
||||
}
|
||||
public AccelerometerData getFirst() {
|
||||
return super.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_L(){
|
||||
return this.stream().mapToLong(d -> d.ts).toArray();
|
||||
}
|
||||
public double[] getTs_D(){
|
||||
return this.stream().mapToDouble(d -> d.ts).toArray();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package de.tonifetzer.conductorswatch.bpmEstimation;
|
||||
package de.tonifetzer.conductorswatch.estimation.accelerometer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
@@ -75,7 +75,7 @@ public class AccelerometerWindowBuffer extends ArrayList<AccelerometerData> {
|
||||
public AccelerometerWindowBuffer getFixedSizedWindow(int size, int overlap){
|
||||
AccelerometerWindowBuffer other = new AccelerometerWindowBuffer(size, overlap);
|
||||
|
||||
double sampleRate = ((getYongest().ts - getOldest().ts) / super.size());
|
||||
double sampleRate = (double) ((getYongest().ts - getOldest().ts) / super.size());
|
||||
|
||||
//if current size is smaller then wanted size, start at 0 and provide smaller list
|
||||
int start = 0;
|
||||
@@ -0,0 +1,110 @@
|
||||
package de.tonifetzer.conductorswatch.estimation.accelerometer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
|
||||
public class AccelerometerWindowContainer {
|
||||
|
||||
private int mWindowSize;
|
||||
private int mOverlapSize;
|
||||
private int mOverlapCounter;
|
||||
|
||||
//data arrays that provide fixed windows for calculation
|
||||
private AccelerometerWindow mRawData;
|
||||
private AccelerometerWindow mInterpolatedData;
|
||||
|
||||
//buffer that is permanently updated through incoming sensor data
|
||||
private LinkedList<AccelerometerData> mBuffer;
|
||||
|
||||
public AccelerometerWindowContainer(int windowSize_ms, int overlap_ms){
|
||||
mWindowSize = windowSize_ms;
|
||||
mOverlapSize = overlap_ms;
|
||||
mOverlapCounter = 0;
|
||||
|
||||
mBuffer = new LinkedList<>();
|
||||
}
|
||||
|
||||
public boolean add(AccelerometerData ad){
|
||||
//TODO: add exception handling. falseArgument if ad has no numeric x,y,z
|
||||
|
||||
synchronized (this){
|
||||
|
||||
//do not add duplicates!
|
||||
if(!mBuffer.isEmpty() && mBuffer.getLast().equals(ad)){
|
||||
return false;
|
||||
}
|
||||
|
||||
// current - last to increment overlap time
|
||||
if(!mBuffer.isEmpty()){
|
||||
mOverlapCounter += ad.ts - mBuffer.getLast().ts;
|
||||
}
|
||||
|
||||
//add element
|
||||
boolean r = mBuffer.add(ad);
|
||||
removeOldElements();
|
||||
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
private void removeOldElements(){
|
||||
synchronized (this) {
|
||||
if (!mBuffer.isEmpty()) {
|
||||
if ((mBuffer.getLast().ts - mBuffer.getFirst().ts) > mWindowSize) {
|
||||
|
||||
while(mBuffer.getFirst().ts < mBuffer.getLast().ts - mWindowSize){
|
||||
mBuffer.remove(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isNextWindowReady(){
|
||||
|
||||
if(!mBuffer.isEmpty()){
|
||||
if(((mBuffer.getFirst().ts - mBuffer.getLast().ts) > mWindowSize / 4) && mOverlapCounter > mOverlapSize){
|
||||
mOverlapCounter = 0;
|
||||
|
||||
//fill the data arrays - we use a shallow copy, as the values are not touched.
|
||||
mRawData = new AccelerometerWindow(mBuffer);
|
||||
mInterpolatedData = AccelerometerInterpolator.interpolate(mRawData);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public ArrayList<AccelerometerData> getRawData(){
|
||||
return mRawData;
|
||||
}
|
||||
|
||||
public ArrayList<AccelerometerData> getInterpolatedData(){
|
||||
return mInterpolatedData;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public void clear(){
|
||||
synchronized (this){
|
||||
mBuffer.clear();
|
||||
mRawData.clear();
|
||||
mInterpolatedData.clear();
|
||||
this.mOverlapCounter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package de.tonifetzer.conductorswatch.bpmEstimation;
|
||||
package de.tonifetzer.conductorswatch.estimation.dsp;
|
||||
|
||||
import org.jtransforms.fft.DoubleFFT_1D;
|
||||
import de.tonifetzer.conductorswatch.utilities.Utils;
|
||||
@@ -0,0 +1,67 @@
|
||||
package de.tonifetzer.conductorswatch.estimation.dsp;
|
||||
|
||||
import de.tonifetzer.conductorswatch.estimation.accelerometer.AccelerometerWindow;
|
||||
import de.tonifetzer.conductorswatch.utilities.Utils;
|
||||
import java.util.Arrays;
|
||||
import java.util.DoubleSummaryStatistics;
|
||||
|
||||
/**
|
||||
* Created by toni on 06/12/18.
|
||||
*/
|
||||
public class DistanceCorrelation {
|
||||
|
||||
//TODO: remove bad peaks found at the very beginning and end of the signal
|
||||
private static int mMaxLag;
|
||||
private double[] mCorr;
|
||||
|
||||
public DistanceCorrelation(AccelerometerWindow data, int maxLag){
|
||||
|
||||
mMaxLag = maxLag;
|
||||
mCorr = calc(data);
|
||||
}
|
||||
|
||||
public double[] getCorr(){
|
||||
return mCorr;
|
||||
}
|
||||
|
||||
private double[] calc(AccelerometerWindow data){
|
||||
|
||||
if(mMaxLag < 1){
|
||||
throw new RuntimeException("maxlag has to be greater 1");
|
||||
}
|
||||
|
||||
//init
|
||||
int n = data.size();
|
||||
int lag_size = Math.min(mMaxLag, n - 1);
|
||||
double[] corr = new double[lag_size + 1];
|
||||
|
||||
//do the math
|
||||
for(int j = 0; j <= lag_size; ++j){
|
||||
double[] dist = new double[n - Math.abs(j)];
|
||||
int idx = 0;
|
||||
|
||||
for (int i = j; i < n; ++i){
|
||||
dist[idx] = Utils.getDistance(data.get(i).x, data.get(i).y, data.get(i).z, data.get(i-j).x, data.get(i-j).y, data.get(i-j).z);
|
||||
++idx;
|
||||
}
|
||||
|
||||
corr[j] = Utils.geometricMeanLog(dist);
|
||||
}
|
||||
|
||||
//to [0, 1]
|
||||
DoubleSummaryStatistics corrStat = Arrays.stream(corr).summaryStatistics();
|
||||
double corMaxVal = corrStat.getMax();
|
||||
for(int k = 0; k < corr.length; ++k){
|
||||
corr[k] = ((corr[k] * (-1)) / corMaxVal) + 1;
|
||||
}
|
||||
|
||||
// mirror corr(2:512) and put it in front
|
||||
double[] output = new double[(2 * lag_size) + 1];
|
||||
System.arraycopy(corr, 0, output, lag_size, lag_size + 1); // +1 to place the 1.0 in the middle of correlation
|
||||
Utils.reverse(corr);
|
||||
System.arraycopy(corr, 0, output, 0, lag_size);
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package de.tonifetzer.conductorswatch.bpmEstimation;
|
||||
package de.tonifetzer.conductorswatch.estimation.dsp;
|
||||
|
||||
import de.tonifetzer.conductorswatch.utilities.Utils;
|
||||
|
||||
@@ -7,7 +7,7 @@ import java.util.LinkedList;
|
||||
/**
|
||||
* Created by toni on 15/12/17.
|
||||
*/
|
||||
public class Peaks {
|
||||
public class PeakDetector {
|
||||
|
||||
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
|
||||
@@ -24,7 +24,7 @@ public class Peaks {
|
||||
* @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){
|
||||
public PeakDetector(double[] data, int width, double threshold, double decayRate, boolean isRelative){
|
||||
|
||||
this.mData = data;
|
||||
this.mPeaksIdx = new LinkedList<>();
|
||||
@@ -152,12 +152,15 @@ public class SensorDataFileStreamer implements Runnable{
|
||||
}
|
||||
|
||||
public void close(){
|
||||
try {
|
||||
mRunning = false;
|
||||
mOutputStream.close();
|
||||
mOutputStreamOpened = false;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
if(mChannelOpenend && mNode != null){
|
||||
try {
|
||||
mRunning = false;
|
||||
mOutputStream.close();
|
||||
mOutputStreamOpened = false;
|
||||
} catch (IOException e) {
|
||||
//e.printStackTrace();
|
||||
Log.d(TAG, "Closing the outputstream failed!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package de.tonifetzer.conductorswatch;
|
||||
package de.tonifetzer.conductorswatch.ui;
|
||||
|
||||
import android.animation.ArgbEvaluator;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
@@ -73,6 +72,12 @@ public class Croller extends View {
|
||||
this.mProgressChangeListener = mProgressChangeListener;
|
||||
}
|
||||
|
||||
public interface OnCrollerChangeListener {
|
||||
void onProgressChanged(Croller croller, int progress);
|
||||
void onStartTrackingTouch(Croller croller);
|
||||
void onStopTrackingTouch(Croller croller);
|
||||
}
|
||||
|
||||
public void setOnCrollerChangeListener(OnCrollerChangeListener mCrollerChangeListener) {
|
||||
this.mCrollerChangeListener = mCrollerChangeListener;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package de.tonifetzer.conductorswatch.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Vibrator;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.TimerTask;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* Created by toni on 13/11/17.
|
||||
*/
|
||||
public class Metronome extends TimerTask {
|
||||
|
||||
private Vibrator mVibrator;
|
||||
|
||||
public Metronome(Context context){
|
||||
mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
mVibrator.vibrate(10);
|
||||
|
||||
for (OnMetronomeListener listener:listeners) {
|
||||
listener.onNewClick();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for callback metronome clicks
|
||||
* I know that java has observable.. but this why it is more clean and easy
|
||||
*/
|
||||
public interface OnMetronomeListener {
|
||||
void onNewClick();
|
||||
}
|
||||
|
||||
private List<OnMetronomeListener> listeners = new CopyOnWriteArrayList<OnMetronomeListener>();
|
||||
public void add(OnMetronomeListener listener){listeners.add(listener);}
|
||||
public void remove(OnMetronomeListener listener){listeners.remove(listener);}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
package de.tonifetzer.conductorswatch;
|
||||
|
||||
import android.util.Log;
|
||||
package de.tonifetzer.conductorswatch.ui;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Vector;
|
||||
@@ -13,36 +11,36 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||
public class TapBpm implements Runnable {
|
||||
|
||||
private Vector<Long> mReceivedTabs = new Vector<Long>();
|
||||
private int mBpmTapped;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
int breakCounter = 2000;
|
||||
mBpmTapped = 0;
|
||||
int bpmTapped = 0;
|
||||
int calcNewBpmCounter = 3;
|
||||
|
||||
do{
|
||||
if(mBpmTapped > 0){
|
||||
breakCounter = 2 * (60000 / mBpmTapped);
|
||||
}
|
||||
|
||||
if (mReceivedTabs.size() > calcNewBpmCounter) {
|
||||
|
||||
Long sumDifferenceMs = 0l;
|
||||
for (int i = 0; i < mReceivedTabs.size() -1; ++i) {
|
||||
sumDifferenceMs += mReceivedTabs.get(i + 1)- mReceivedTabs.get(i);
|
||||
}
|
||||
mBpmTapped = (int) (60000 / (sumDifferenceMs / (mReceivedTabs.size() - 1)));
|
||||
|
||||
for (TapBpm.OnTapBpmListener listener:listeners) {
|
||||
listener.onNewTapEstimation(mBpmTapped);
|
||||
if(!mReceivedTabs.isEmpty()){
|
||||
do{
|
||||
if(bpmTapped > 0){
|
||||
breakCounter = 2 * (60000 / bpmTapped);
|
||||
}
|
||||
|
||||
//only update everytime a new timestamp arrives
|
||||
++calcNewBpmCounter;
|
||||
}
|
||||
}while(System.currentTimeMillis() - mReceivedTabs.lastElement() < breakCounter);
|
||||
if (mReceivedTabs.size() > calcNewBpmCounter) {
|
||||
|
||||
long sumDifferenceMs = 0L;
|
||||
for (int i = 0; i < mReceivedTabs.size() -1; ++i) {
|
||||
sumDifferenceMs += mReceivedTabs.get(i + 1)- mReceivedTabs.get(i);
|
||||
}
|
||||
bpmTapped = (int) (60000 / (sumDifferenceMs / (mReceivedTabs.size() - 1)));
|
||||
|
||||
for (TapBpm.OnTapBpmListener listener:listeners) {
|
||||
listener.onNewTapEstimation(bpmTapped);
|
||||
}
|
||||
|
||||
//only update everytime a new timestamp arrives
|
||||
++calcNewBpmCounter;
|
||||
}
|
||||
}while(System.currentTimeMillis() - mReceivedTabs.lastElement() < breakCounter);
|
||||
}
|
||||
|
||||
for (TapBpm.OnTapBpmListener listener:listeners) {
|
||||
listener.onTapFinished();
|
||||
@@ -50,11 +48,11 @@ public class TapBpm implements Runnable {
|
||||
}
|
||||
|
||||
public void addTimestamp(Long ts){
|
||||
mReceivedTabs.add(ts);
|
||||
mReceivedTabs.add(ts);
|
||||
}
|
||||
|
||||
public void clearTimestamps(){
|
||||
mReceivedTabs.clear();
|
||||
mReceivedTabs.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -12,7 +12,27 @@ import java.util.List;
|
||||
//TODO: change from double to generic type
|
||||
public class Utils {
|
||||
public static double getDistance(double x1, double y1, double x2, double y2) {
|
||||
return (double) Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
|
||||
return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
|
||||
}
|
||||
|
||||
public static double getDistance(double x1, double y1, double z1, double x2, double y2, double z2) {
|
||||
return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2) + (z1 - z2) * (z1 - z2));
|
||||
}
|
||||
|
||||
public static void reverse(double[] array) {
|
||||
if (array == null) {
|
||||
return;
|
||||
}
|
||||
int i = 0;
|
||||
int j = array.length - 1;
|
||||
double tmp;
|
||||
while (j > i) {
|
||||
tmp = array[j];
|
||||
array[j] = array[i];
|
||||
array[i] = tmp;
|
||||
j--;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
public static double sqr(double x) {
|
||||
@@ -120,6 +140,17 @@ public class Utils {
|
||||
return Math.pow(sum, 1.0 / data.length);
|
||||
}
|
||||
|
||||
public static double geometricMeanLog(double[] data){
|
||||
double GM_log = 0.0d;
|
||||
for (int i = 0; i < data.length; ++i) {
|
||||
if (data[i] == 0.0d) {
|
||||
return 0.0d;
|
||||
}
|
||||
GM_log += Math.log(data[i]);
|
||||
}
|
||||
return Math.exp(GM_log / data.length);
|
||||
}
|
||||
|
||||
public static int intersectionNumber(double[] signal, double border){
|
||||
int cnt = 0;
|
||||
boolean isSmallerValue = false;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
||||
<de.tonifetzer.conductorswatch.Croller
|
||||
<de.tonifetzer.conductorswatch.ui.Croller
|
||||
android:id="@+id/croller"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
||||
@@ -10,7 +10,7 @@ buildscript {
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.1.2'
|
||||
classpath 'com.android.tools.build:gradle:3.3.0'
|
||||
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#Fri Apr 27 14:10:48 CEST 2018
|
||||
#Sun Jan 27 12:15:23 CET 2019
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import bpmEstimation.*;
|
||||
import uk.me.berndporr.iirj.Butterworth;
|
||||
import utilities.Plot;
|
||||
import utilities.Utils;
|
||||
|
||||
import java.awt.*;
|
||||
@@ -6,7 +8,10 @@ import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.text.ParseException;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Vector;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
|
||||
@@ -16,34 +21,64 @@ import java.util.stream.IntStream;
|
||||
public class Main {
|
||||
|
||||
public static void main(String [ ] args) {
|
||||
//File folder = new File("/home/toni/Documents/programme/dirigent/measurements/lgWear");
|
||||
File folder = new File("/home/toni/Documents/programme/dirigent/measurements/peter_failed");
|
||||
//File folder = new File("/home/toni/Documents/programme/dirigent/measurements/2017.06/lgWear");
|
||||
//File folder = new File("/home/toni/Documents/programme/dirigent/measurements/peter_failed");
|
||||
File folder = new File("/home/toni/Documents/programme/dirigent/measurements/2018.06/frank/mSensorTest");
|
||||
File[] listOfFiles = folder.listFiles();
|
||||
Arrays.sort(listOfFiles);
|
||||
|
||||
Utils.ShowPNG windowRaw = new Utils.ShowPNG();
|
||||
Utils.ShowPNG windowAuto = new Utils.ShowPNG();
|
||||
Utils.ShowPNG windowPeaksX = new Utils.ShowPNG();
|
||||
Utils.ShowPNG windowPeaksY = new Utils.ShowPNG();
|
||||
Utils.ShowPNG windowPeaksZ = new Utils.ShowPNG();
|
||||
|
||||
//calc results
|
||||
BpmHistory historyAll = new BpmHistory();
|
||||
BpmHistory historyMag = new BpmHistory();
|
||||
BpmHistory history3D = new BpmHistory();
|
||||
|
||||
// iterate trough files in measurements folder
|
||||
for (File file : listOfFiles) {
|
||||
if (file.isFile() && file.getName().contains(".csv")) {
|
||||
|
||||
//TODO: mach Fenster genau 6 sekunden groß. Egal wie viele Samples.
|
||||
AccelerometerWindowBuffer accWindowBuffer = new AccelerometerWindowBuffer(6000, 1500);
|
||||
BpmEstimator bpmEstimator = new BpmEstimator(accWindowBuffer, 0, 5000);
|
||||
AccelerometerWindowBuffer accWindowBuffer = new AccelerometerWindowBuffer(6000, 875);
|
||||
BpmEstimator bpmEstimator = new BpmEstimator(accWindowBuffer, 4, 50000);
|
||||
|
||||
//read the file line by line
|
||||
try (BufferedReader br = new BufferedReader(new FileReader(file))) {
|
||||
|
||||
//read the first three lines and print out what file it is!
|
||||
String comment = br.readLine();
|
||||
String groundTruthLine = br.readLine();
|
||||
br.readLine();
|
||||
System.out.println(comment);
|
||||
System.out.println(file.getName());
|
||||
|
||||
//load ground truth file
|
||||
final long startTs = Long.parseLong(br.readLine().split(";")[0]);
|
||||
String gtFile = groundTruthLine.substring(groundTruthLine.indexOf(':') + 2);
|
||||
Utils.GroundTruthData gtData = new Utils.GroundTruthData();
|
||||
double gtCurValue = 0d;
|
||||
|
||||
if (gtFile.contains(".csv")) {
|
||||
try (BufferedReader gtBr = new BufferedReader(new FileReader("../../measurements/2018.06/gt_toni/" + gtFile))) {
|
||||
for (String gtLine; (gtLine = gtBr.readLine()) != null; ) {
|
||||
gtData.setValuesFromString(gtLine);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} catch (ParseException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
} else {
|
||||
gtData.setSingleBPM(Double.valueOf(gtFile));
|
||||
}
|
||||
|
||||
//read sensor measurements line by line
|
||||
for (String line; (line = br.readLine()) != null; ) {
|
||||
// process the line.
|
||||
String[] measurement = line.split(";");
|
||||
|
||||
//if linear acc
|
||||
if(measurement[1].equals("10")){
|
||||
long ts = Long.parseLong(measurement[0]);
|
||||
long ts = 0;
|
||||
if (measurement[1].equals("3")) {
|
||||
ts = Long.parseLong(measurement[0]);
|
||||
double x = Double.parseDouble(measurement[2]);
|
||||
double y = Double.parseDouble(measurement[3]);
|
||||
double z = Double.parseDouble(measurement[4]);
|
||||
@@ -51,97 +86,62 @@ public class Main {
|
||||
}
|
||||
|
||||
//do calculation stuff
|
||||
if(accWindowBuffer.isNextWindowReady()){
|
||||
if (accWindowBuffer.isNextWindowReady()) {
|
||||
|
||||
double curBpm = bpmEstimator.estimate();
|
||||
//System.out.println("BPM: " + curBpm);
|
||||
LinkedList<Double> bpmList = new LinkedList<>();
|
||||
|
||||
double sampleRate = 20;
|
||||
AccelerometerInterpolator acInterp = new AccelerometerInterpolator(accWindowBuffer, sampleRate);
|
||||
int peakWidth = (int) Math.round(250 / sampleRate);
|
||||
// Calculate the BPM for different window sizes
|
||||
double bpm60 = bpmEstimator.estimate();
|
||||
double bpm85 = bpmEstimator.estimate(3500, 875);
|
||||
double bpm110 = bpmEstimator.estimate(2600, 875);
|
||||
double bpm135 = bpmEstimator.estimate(2000, 875);
|
||||
double bpm160 = bpmEstimator.estimate(1600, 875);
|
||||
double bpm200 = bpmEstimator.estimate(1200, 875);
|
||||
|
||||
//print raw x,y,z
|
||||
double[] dTs = IntStream.range(0, accWindowBuffer.getTs().length).mapToDouble(i -> accWindowBuffer.getTs()[i]).toArray();
|
||||
double[] dTsInterp = IntStream.range(0, acInterp.getTs().length).mapToDouble(i -> acInterp.getTs()[i]).toArray();
|
||||
Plot plotRaw = Plot.plot(Plot.plotOpts().
|
||||
title("Raw Acc Data").
|
||||
legend(Plot.LegendFormat.BOTTOM)).
|
||||
series("x", Plot.data().xy(dTsInterp, acInterp.getX()), Plot.seriesOpts().color(Color.RED)).
|
||||
series("y", Plot.data().xy(dTsInterp, acInterp.getY()), Plot.seriesOpts().color(Color.BLUE)).
|
||||
series("z", Plot.data().xy(dTsInterp, acInterp.getZ()), Plot.seriesOpts().color(Color.GREEN));
|
||||
//System.out.println("--------------------------------------------------");
|
||||
|
||||
windowRaw.set(plotRaw.draw());
|
||||
bpmList.add(bpm60);
|
||||
bpmList.add(bpm85);
|
||||
bpmList.add(bpm110);
|
||||
bpmList.add(bpm135);
|
||||
bpmList.add(bpm160);
|
||||
bpmList.add(bpm200);
|
||||
|
||||
//auto corr
|
||||
double[] xAutoCorr = new AutoCorrelation(acInterp.getX(), accWindowBuffer.size()).getCorr();
|
||||
double[] yAutoCorr = new AutoCorrelation(acInterp.getY(), accWindowBuffer.size()).getCorr();
|
||||
double[] zAutoCorr = new AutoCorrelation(acInterp.getZ(), accWindowBuffer.size()).getCorr();
|
||||
while (bpmList.remove(Double.valueOf(-1))) {
|
||||
}
|
||||
Utils.removeOutliersZScore(bpmList, 3.4);
|
||||
|
||||
//print autocorr
|
||||
int[] tmp = IntStream.rangeClosed(-((xAutoCorr.length - 1)/2), ((xAutoCorr.length - 1)/2)).toArray();
|
||||
double[] rangeAuto = IntStream.range(0, tmp.length).mapToDouble(i -> tmp[i]).toArray();
|
||||
Plot plotCorr = Plot.plot(Plot.plotOpts().
|
||||
title("Auto Correlation").
|
||||
legend(Plot.LegendFormat.BOTTOM)).
|
||||
series("x", Plot.data().xy(rangeAuto, xAutoCorr), Plot.seriesOpts().color(Color.RED)).
|
||||
series("y", Plot.data().xy(rangeAuto, yAutoCorr), Plot.seriesOpts().color(Color.BLUE)).
|
||||
series("z", Plot.data().xy(rangeAuto, zAutoCorr), Plot.seriesOpts().color(Color.GREEN));
|
||||
double bpmMean = Utils.mean(bpmList);
|
||||
double magMean = bpmEstimator.getMagnitudeMean();
|
||||
double bpmDist = bpmEstimator.getDistEstimation();
|
||||
|
||||
windowAuto.set(plotCorr.draw());
|
||||
//double bpmSingle = bpmEstimator.getBestSingleAxis();
|
||||
double bpmAllAverage = bpmEstimator.getAverageOfAllWindows();
|
||||
|
||||
Peaks pX = new Peaks(xAutoCorr, peakWidth, 0.1f, 0, false);
|
||||
int xOffset = xAutoCorr.length / 2;
|
||||
LinkedList<Integer> peaksX = pX.getPeaksIdx();
|
||||
//System.out.println( ts + " all: " + Math.round(bpmMean) + " avg_all: " + Math.round(bpmAllAverage) + " 3D: " + Math.round(bpmDist));
|
||||
//System.out.println(" ");
|
||||
|
||||
double[] dPeaksXX = IntStream.range(0, peaksX.size()).mapToDouble(i -> (peaksX.get(i) - xOffset)).toArray();//peaks.stream().mapToDouble(i->i).toArray();
|
||||
double[] dPeaksXY = IntStream.range(0, peaksX.size()).mapToDouble(i -> (xAutoCorr[peaksX.get(i)])).toArray();
|
||||
Plot plotPeaksX = Plot.plot(Plot.plotOpts().
|
||||
title("Peak Detection on X").
|
||||
legend(Plot.LegendFormat.BOTTOM)).
|
||||
series("x", Plot.data().xy(rangeAuto, xAutoCorr), Plot.seriesOpts().color(Color.RED)).
|
||||
series("Peaks", Plot.data().xy(dPeaksXX, dPeaksXY), Plot.seriesOpts().color(Color.CYAN).
|
||||
marker(Plot.Marker.DIAMOND).line(Plot.Line.NONE));
|
||||
//calc error using ground truth
|
||||
long curTS = accWindowBuffer.getYongest().ts - startTs;
|
||||
int idx = 0;
|
||||
while (curTS > gtData.getTimestamp(idx) && idx < gtData.getSize() - 1) {
|
||||
++idx;
|
||||
}
|
||||
gtCurValue = gtData.getBPM(idx);
|
||||
|
||||
windowPeaksX.set(plotPeaksX.draw());
|
||||
//fill histories
|
||||
historyAll.add(bpmAllAverage - gtCurValue);
|
||||
historyMag.add(magMean - gtCurValue);
|
||||
history3D.add(bpmDist - gtCurValue);
|
||||
|
||||
Peaks pY = new Peaks(yAutoCorr, peakWidth, 0.1f, 0, false);
|
||||
int yOffset = yAutoCorr.length / 2;
|
||||
LinkedList<Integer> peaksY = pY.getPeaksIdx();
|
||||
|
||||
double[] dPeaksYX = IntStream.range(0, peaksY.size()).mapToDouble(i -> (peaksY.get(i) - yOffset)).toArray();//peaks.stream().mapToDouble(i->i).toArray();
|
||||
double[] dPeaksYY = IntStream.range(0, peaksY.size()).mapToDouble(i -> (yAutoCorr[peaksY.get(i)])).toArray();
|
||||
Plot plotPeaksY = Plot.plot(Plot.plotOpts().
|
||||
title("Peak Detection on Y").
|
||||
legend(Plot.LegendFormat.BOTTOM)).
|
||||
series("x", Plot.data().xy(rangeAuto, yAutoCorr), Plot.seriesOpts().color(Color.RED)).
|
||||
series("Peaks", Plot.data().xy(dPeaksYX, dPeaksYY), Plot.seriesOpts().color(Color.CYAN).
|
||||
marker(Plot.Marker.DIAMOND).line(Plot.Line.NONE));
|
||||
|
||||
windowPeaksY.set(plotPeaksY.draw());
|
||||
|
||||
Peaks pZ = new Peaks(zAutoCorr, peakWidth, 0.1f, 0, false);
|
||||
int zOffset = zAutoCorr.length / 2;
|
||||
LinkedList<Integer> peaksZ = pZ.getPeaksIdx();
|
||||
|
||||
double[] dPeaksZX = IntStream.range(0, peaksZ.size()).mapToDouble(i -> (peaksZ.get(i) - zOffset)).toArray();//peaks.stream().mapToDouble(i->i).toArray();
|
||||
double[] dPeaksZY = IntStream.range(0, peaksZ.size()).mapToDouble(i -> (zAutoCorr[peaksZ.get(i)])).toArray();
|
||||
Plot plotPeaksZ = Plot.plot(Plot.plotOpts().
|
||||
title("Peak Detection on Z").
|
||||
legend(Plot.LegendFormat.BOTTOM)).
|
||||
series("x", Plot.data().xy(rangeAuto, zAutoCorr), Plot.seriesOpts().color(Color.RED)).
|
||||
series("Peaks", Plot.data().xy(dPeaksZX, dPeaksZY), Plot.seriesOpts().color(Color.CYAN).
|
||||
marker(Plot.Marker.DIAMOND).line(Plot.Line.NONE));
|
||||
|
||||
windowPeaksZ.set(plotPeaksZ.draw());
|
||||
|
||||
//fill hols improve peaks
|
||||
|
||||
//estimate bpm between detected peaks
|
||||
//System.out.println("BPM-X: " + pX.getBPM(bpmEstimator.getSampleRate_ms()));
|
||||
//System.out.println("BPM-Y: " + pY.getBPM(bpmEstimator.getSampleRate_ms()));
|
||||
//System.out.println("BPM-Z: " + pZ.getBPM(bpmEstimator.getSampleRate_ms()));
|
||||
|
||||
//todo: kleiner fenstergrößen testen. so ist doch etwas langsam auf der Uhr.
|
||||
if(true){
|
||||
System.out.println("all: " + bpmAllAverage);
|
||||
System.out.println("mag: " + magMean);
|
||||
System.out.println("3D: " + bpmDist);
|
||||
System.out.println("GT: " + gtCurValue);
|
||||
System.out.println(" ");
|
||||
}
|
||||
|
||||
int dummyForBreakpoint = 0;
|
||||
}
|
||||
@@ -153,11 +153,32 @@ public class Main {
|
||||
//System.out.println("MEAN BPM: " + Math.round(meanBPM));
|
||||
//System.out.println("MEDIAN BPM: " + Math.round(medianBPM));
|
||||
|
||||
if (Utils.DEBUG_MODE) {
|
||||
bpmEstimator.closeDebugWindows();
|
||||
}
|
||||
|
||||
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
//print overall stats for a single data series
|
||||
System.out.println("all: " + historyAll.getMean() + "(" + historyAll.getStd() + ")");
|
||||
System.out.println("mag: " + historyMag.getMean() + "(" + historyMag.getStd() + ")");
|
||||
System.out.println(" 3D: " + history3D.getMean() + "(" + history3D.getStd() + ")");
|
||||
System.out.println(" ");
|
||||
|
||||
history3D.clear();
|
||||
historyMag.clear();
|
||||
historyAll.clear();
|
||||
|
||||
}
|
||||
|
||||
// try {
|
||||
// System.in.read();
|
||||
// } catch (IOException e) {
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,12 @@ public class AccelerometerInterpolator {
|
||||
private double[] mY;
|
||||
private double[] mZ;
|
||||
private long[] mTsInterp;
|
||||
private int size;
|
||||
|
||||
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];
|
||||
this.size = (int) ((ab.getYongest().ts - (ab.getOldest().ts - (long) sampleRate_ms)) / (long) sampleRate_ms);
|
||||
mTsInterp = new long[size];
|
||||
int j = 0;
|
||||
for(long i = ab.getOldest().ts; i <= ab.getYongest().ts; i += sampleRate_ms){
|
||||
mTsInterp[j++] = i;
|
||||
@@ -42,6 +43,10 @@ public class AccelerometerInterpolator {
|
||||
return mZ;
|
||||
}
|
||||
|
||||
public int size(){
|
||||
return size;
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
@@ -2,14 +2,15 @@ package bpmEstimation;
|
||||
|
||||
import utilities.Utils;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by toni on 15/12/17.
|
||||
*/
|
||||
public class AccelerometerWindowBuffer extends ArrayList<AccelerometerData> {
|
||||
|
||||
private static int mWindowSize; // in ms
|
||||
private static int mOverlapSize; // in ms
|
||||
private final int mWindowSize; // in ms
|
||||
private final int mOverlapSize; // in ms
|
||||
private long mOverlapCounter;
|
||||
|
||||
public AccelerometerWindowBuffer(int windowSize, int overlap){
|
||||
@@ -18,37 +19,55 @@ public class AccelerometerWindowBuffer extends ArrayList<AccelerometerData> {
|
||||
mOverlapCounter = 0;
|
||||
}
|
||||
|
||||
public AccelerometerWindowBuffer(List<AccelerometerData> list, int overlap){
|
||||
mWindowSize = list.size();
|
||||
mOverlapSize = overlap;
|
||||
mOverlapCounter = 0;
|
||||
super.addAll(list);
|
||||
}
|
||||
|
||||
//TODO: add exception handling. falseArgument if ad has no numeric x,y,z
|
||||
public boolean add(AccelerometerData ad){
|
||||
synchronized (this){
|
||||
|
||||
//do not add duplicates!
|
||||
if(!isEmpty() && getYongest().equals(ad)){
|
||||
return false;
|
||||
//do not add duplicates!
|
||||
if(!isEmpty() && getYongest().equals(ad)){
|
||||
return false;
|
||||
}
|
||||
|
||||
// current - last to increment overlap time
|
||||
if(!isEmpty()){
|
||||
mOverlapCounter += ad.ts - getYongest().ts;
|
||||
}
|
||||
|
||||
//add element
|
||||
boolean r = super.add(ad);
|
||||
removeOldElements();
|
||||
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
// current - last to increment overlap time
|
||||
if(!isEmpty()){
|
||||
mOverlapCounter += ad.ts - getYongest().ts;
|
||||
}
|
||||
private void removeOldElements(){
|
||||
synchronized (this) {
|
||||
if (!isEmpty()) {
|
||||
if ((getYongest().ts - getOldest().ts) > mWindowSize) {
|
||||
|
||||
//add element
|
||||
boolean r = super.add(ad);
|
||||
if ((getYongest().ts - getOldest().ts) > mWindowSize){
|
||||
|
||||
long oldestTime = getYongest().ts - mWindowSize;
|
||||
for(int i = 0; i < size(); ++i) {
|
||||
if (get(i).ts > oldestTime) {
|
||||
break;
|
||||
long oldestTime = getYongest().ts - mWindowSize;
|
||||
for (int i = 0; i < size(); ++i) {
|
||||
if (get(i).ts > oldestTime) {
|
||||
break;
|
||||
}
|
||||
super.remove(i);
|
||||
}
|
||||
}
|
||||
remove(i);
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
public boolean isNextWindowReady(){
|
||||
if(!isEmpty()){
|
||||
if(((getYongest().ts - getOldest().ts) > mWindowSize / 2) && mOverlapCounter > mOverlapSize){
|
||||
if(((getYongest().ts - getOldest().ts) > mWindowSize) && mOverlapCounter > mOverlapSize){
|
||||
mOverlapCounter = 0;
|
||||
|
||||
return true;
|
||||
@@ -57,6 +76,31 @@ public class AccelerometerWindowBuffer extends ArrayList<AccelerometerData> {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static AccelerometerWindowBuffer getNewInstance(AccelerometerWindowBuffer buffer, int size, int overlap) {
|
||||
|
||||
double sampleRate = ((buffer.getYongest().ts - buffer.getOldest().ts) / buffer.size());
|
||||
|
||||
//if current size is smaller then wanted size, start at 0 and provide smaller list
|
||||
int start = 0;
|
||||
if ((buffer.getYongest().ts - buffer.getOldest().ts) > size) {
|
||||
start = (int) Math.round(buffer.size() - (size / sampleRate));
|
||||
}
|
||||
|
||||
// start should not be negative, this can happen due to rounding errors.
|
||||
start = start < 0 ? 0 : start;
|
||||
|
||||
List<AccelerometerData> syncList;
|
||||
synchronized (buffer) {
|
||||
syncList = buffer.subList(start, buffer.size());
|
||||
}
|
||||
|
||||
return new AccelerometerWindowBuffer(syncList, overlap);
|
||||
}
|
||||
|
||||
public static AccelerometerWindowBuffer getNewInstance(AccelerometerWindowBuffer buffer) {
|
||||
return new AccelerometerWindowBuffer(buffer, buffer.getOverlapSize());
|
||||
}
|
||||
|
||||
public AccelerometerData getYongest() {
|
||||
return get(size() - 1);
|
||||
}
|
||||
@@ -84,4 +128,6 @@ public class AccelerometerWindowBuffer extends ArrayList<AccelerometerData> {
|
||||
public int getOverlapSize(){
|
||||
return mOverlapSize;
|
||||
}
|
||||
|
||||
public int getWindowSize(){return mWindowSize; }
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
package bpmEstimation;
|
||||
|
||||
import static utilities.Utils.DEBUG_MODE;
|
||||
|
||||
import utilities.MovingFilter;
|
||||
import utilities.SimpleKalman;
|
||||
import utilities.Utils;
|
||||
|
||||
import uk.me.berndporr.iirj.*;
|
||||
|
||||
import java.util.LinkedList;
|
||||
|
||||
/**
|
||||
@@ -14,9 +17,11 @@ public class BpmEstimator {
|
||||
private AccelerometerWindowBuffer mBuffer;
|
||||
private static double mSampleRate_ms;
|
||||
|
||||
private LinkedList<Double> mBpmHistory_X;
|
||||
private LinkedList<Double> mBpmHistory_Y;
|
||||
private LinkedList<Double> mBpmHistory_Z;
|
||||
private BpmHistory mBpmHistory_X;
|
||||
private BpmHistory mBpmHistory_Y;
|
||||
private BpmHistory mBpmHistory_Z;
|
||||
private BpmHistory mBpmHistory_Mag;
|
||||
private BpmHistory mBpmHistory_Dist;
|
||||
|
||||
private LinkedList<Double> mBpmHistory;
|
||||
private int mResetCounter;
|
||||
@@ -25,47 +30,139 @@ public class BpmEstimator {
|
||||
private MovingFilter mMvg;
|
||||
//private SimpleKalman mKalman;
|
||||
|
||||
private Butterworth mButter_X;
|
||||
private Butterworth mButter_Y;
|
||||
private Butterworth mButter_Z;
|
||||
private Butterworth mButter_Mag;
|
||||
|
||||
|
||||
//todo: hack
|
||||
private double magnitudeMean = 1.0;
|
||||
|
||||
//Debugging stuff
|
||||
private Utils.DebugPlotter plotter;
|
||||
|
||||
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_X = new BpmHistory();
|
||||
mBpmHistory_Y = new BpmHistory();
|
||||
mBpmHistory_Z = new BpmHistory();
|
||||
mBpmHistory_Mag = new BpmHistory();
|
||||
mBpmHistory_Dist = new BpmHistory();
|
||||
|
||||
mBpmHistory = new LinkedList<>();
|
||||
mBpmHistory = new BpmHistory();
|
||||
mResetCounter = 0;
|
||||
mResetLimit_ms = resetAfter_ms;
|
||||
|
||||
mMvg = new MovingFilter(10);
|
||||
mMvg = new MovingFilter(2);
|
||||
//mKalman = new SimpleKalman();
|
||||
|
||||
mButter_X = new Butterworth();
|
||||
mButter_Y = new Butterworth();
|
||||
mButter_Z = new Butterworth();
|
||||
mButter_Mag = new Butterworth();
|
||||
|
||||
if(DEBUG_MODE){
|
||||
plotter = new Utils.DebugPlotter();
|
||||
}
|
||||
}
|
||||
|
||||
public double estimate(){
|
||||
return estimate(mBuffer.getWindowSize(), mBuffer.getOverlapSize());
|
||||
}
|
||||
|
||||
public double estimate(int length_ms, int overlap_ms){
|
||||
|
||||
//todo: interpolator könnte man direkt vom buffer zurück geben lassen.
|
||||
//todo: anstatt hier eine zweite instance zu öffnen, einfach ein globales format wie bpsw: AccelerometerWindow
|
||||
AccelerometerWindowBuffer tmpBuffer = AccelerometerWindowBuffer.getNewInstance(mBuffer, length_ms, overlap_ms);
|
||||
|
||||
double sampleRate = mSampleRate_ms;
|
||||
if(sampleRate <= 0){
|
||||
sampleRate = Math.round(Utils.mean(Utils.diff(mBuffer.getTs())));
|
||||
sampleRate = Math.round(Utils.mean(Utils.diff(tmpBuffer.getTs())));
|
||||
}
|
||||
|
||||
AccelerometerInterpolator interp = new AccelerometerInterpolator(mBuffer, sampleRate);
|
||||
assert sampleRate != 0 : "samplerate is zero";
|
||||
|
||||
double[] xAutoCorr = new AutoCorrelation(interp.getX(), mBuffer.size()).getCorr();
|
||||
double[] yAutoCorr = new AutoCorrelation(interp.getY(), mBuffer.size()).getCorr();
|
||||
double[] zAutoCorr = new AutoCorrelation(interp.getZ(), mBuffer.size()).getCorr();
|
||||
// interpolate
|
||||
AccelerometerInterpolator interp = new AccelerometerInterpolator(tmpBuffer, sampleRate);
|
||||
double[] magRaw = Utils.magnitude(interp);
|
||||
|
||||
//todo: aufräumen. funktion die eine achse bekommt und aus ihr dann die peaks zurück gibt. für debuggen gleich zeichenfunktionen dazu.
|
||||
|
||||
// butterworth lowpass filter, cutoff at 3 hz (~180 bpm)
|
||||
mButter_X.lowPass(1,(1/(sampleRate / 1000))/2, 1);
|
||||
mButter_Y.lowPass(1,(1/(sampleRate / 1000))/2, 1);
|
||||
mButter_Z.lowPass(1,(1/(sampleRate / 1000))/2, 1);
|
||||
mButter_Mag.lowPass(1,(1/(sampleRate / 1000))/2, 1);
|
||||
|
||||
int n = interp.getX().length;
|
||||
double[] xButter = new double[n];
|
||||
double[] yButter = new double[n];
|
||||
double[] zButter = new double[n];
|
||||
double[] magButter = new double[n];
|
||||
for(int i = 0; i < interp.getX().length; ++i){
|
||||
xButter[i] = mButter_X.filter(interp.getX()[i]);
|
||||
yButter[i] = mButter_Y.filter(interp.getY()[i]);
|
||||
zButter[i] = mButter_Z.filter(interp.getZ()[i]);
|
||||
magButter[i] = mButter_Mag.filter(magRaw[i]);
|
||||
}
|
||||
|
||||
|
||||
//auto correlation
|
||||
double[] xAutoCorr = new AutoCorrelation(xButter, tmpBuffer.size()).getCorr();
|
||||
double[] yAutoCorr = new AutoCorrelation(yButter, tmpBuffer.size()).getCorr();
|
||||
double[] zAutoCorr = new AutoCorrelation(zButter, tmpBuffer.size()).getCorr();
|
||||
double[] magAutoCorr = new AutoCorrelation(magButter, tmpBuffer.size()).getCorr();
|
||||
|
||||
//dist correlation
|
||||
double[] distCorr = new DistanceCorrelation(interp, (int) (interp.size() / 2)).getCorr();
|
||||
|
||||
//find a peak within range of 250 ms
|
||||
int peakWidth = (int) Math.round(250 / sampleRate);
|
||||
Peaks pX = new Peaks(xAutoCorr, peakWidth, 0.1f, 0, false);
|
||||
Peaks pY = new Peaks(yAutoCorr, peakWidth, 0.1f, 0, false);
|
||||
Peaks pZ = new Peaks(zAutoCorr, peakWidth, 0.1f, 0, false);
|
||||
Peaks xPeaks = new Peaks(xAutoCorr, peakWidth, 0.1f, 0, false);
|
||||
Peaks yPeaks = new Peaks(yAutoCorr, peakWidth, 0.1f, 0, false);
|
||||
Peaks zPeaks = new Peaks(zAutoCorr, peakWidth, 0.1f, 0, false);
|
||||
Peaks magPeaks = new Peaks(magAutoCorr, peakWidth, 0.1f, 0, false);
|
||||
|
||||
mBpmHistory_X.add(pX.getBPM(sampleRate));
|
||||
mBpmHistory_Y.add(pY.getBPM(sampleRate));
|
||||
mBpmHistory_Z.add(pZ.getBPM(sampleRate));
|
||||
Peaks distPeaks = new Peaks(distCorr, peakWidth, 0.1f, 0, false);
|
||||
|
||||
double estimatedBPM = getBestBpmEstimation(pX, pY, pZ);
|
||||
mBpmHistory_X.add(xPeaks.getBPM(sampleRate));
|
||||
mBpmHistory_Y.add(yPeaks.getBPM(sampleRate));
|
||||
mBpmHistory_Z.add(zPeaks.getBPM(sampleRate));
|
||||
mBpmHistory_Mag.add(magPeaks.getBPM(sampleRate));
|
||||
mBpmHistory_Dist.add(distPeaks.getBPM(sampleRate));
|
||||
|
||||
|
||||
if(DEBUG_MODE){
|
||||
plotter.setPlotRawX(interp.getTs(), interp.getX());
|
||||
plotter.setPlotRawY(interp.getTs(), interp.getY());
|
||||
plotter.setPlotRawZ(interp.getTs(), interp.getZ());
|
||||
plotter.setPlotRawMag(interp.getTs(), magRaw);
|
||||
|
||||
plotter.setPlotButterX(interp.getTs(), xButter);
|
||||
plotter.setPlotButterY(interp.getTs(), yButter);
|
||||
plotter.setPlotButterZ(interp.getTs(), zButter);
|
||||
plotter.setPlotButterMag(interp.getTs(), magButter);
|
||||
|
||||
plotter.setPlotCorrX(xAutoCorr, xPeaks);
|
||||
plotter.setPlotCorrY(yAutoCorr, yPeaks);
|
||||
plotter.setPlotCorrZ(zAutoCorr, zPeaks);
|
||||
plotter.setPlotCorrMag(magAutoCorr, magPeaks);
|
||||
plotter.setPlotCorr3D(distCorr, distPeaks);
|
||||
|
||||
//printout the current BPM
|
||||
System.out.println(length_ms + "; x: " + Math.round(xPeaks.getBPM(sampleRate))
|
||||
+ "; y: " + Math.round(yPeaks.getBPM(sampleRate))
|
||||
+ "; z: " + Math.round(zPeaks.getBPM(sampleRate))
|
||||
+ "; mag: " + Math.round(magPeaks.getBPM(sampleRate))
|
||||
+ "; 3D: " + Math.round(distPeaks.getBPM(sampleRate)));
|
||||
}
|
||||
|
||||
|
||||
double estimatedBPM = getBestBpmEstimation(xPeaks, yPeaks, zPeaks, magPeaks);
|
||||
if(estimatedBPM != -1){
|
||||
|
||||
//moving avg (lohnt dann, wenn wir viele daten haben)
|
||||
@@ -86,7 +183,8 @@ public class BpmEstimator {
|
||||
if(++mResetCounter > resetAfter){
|
||||
mBpmHistory.clear();
|
||||
|
||||
mBuffer.clear();
|
||||
//TODO: send signal to clear from outside this function should just return the bpm
|
||||
//mBuffer.clear();
|
||||
mMvg.clear();
|
||||
mResetCounter = 0;
|
||||
}
|
||||
@@ -97,6 +195,33 @@ public class BpmEstimator {
|
||||
return mBpmHistory.getLast();
|
||||
}
|
||||
|
||||
public double getDistEstimation(){
|
||||
|
||||
if(!mBpmHistory_Dist.isEmpty()){
|
||||
BpmHistory tmp = (BpmHistory) mBpmHistory_Dist.clone();
|
||||
tmp.removeOutliers();
|
||||
return tmp.getMean();
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public double getMagnitudeMean(){
|
||||
//while(mBpmHistory_Mag.remove(Double.valueOf(-1))) {}
|
||||
//Utils.removeOutliersZScore(mBpmHistory_Mag, 3.4);
|
||||
//double mean = Utils.mean(mBpmHistory_Mag);
|
||||
//mBpmHistory_Mag.clear();
|
||||
|
||||
if(!mBpmHistory_Mag.isEmpty()){
|
||||
BpmHistory tmp = (BpmHistory) mBpmHistory_Mag.clone();
|
||||
tmp.removeOutliers();
|
||||
return tmp.getMean();
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public double getMeanBpm(){
|
||||
return Utils.mean(mBpmHistory);
|
||||
}
|
||||
@@ -105,7 +230,88 @@ public class BpmEstimator {
|
||||
return Utils.median(mBpmHistory);
|
||||
}
|
||||
|
||||
private double getBestBpmEstimation(Peaks peaksX, Peaks peaksY, Peaks peaksZ) throws IllegalArgumentException {
|
||||
//call this after the different estimations are done
|
||||
//1) nimm die achse mit der geringsten varianz der bpm schätzung über alle fenster
|
||||
//2) schmeiße outliers raus
|
||||
//3) berechne mean -> profit?!
|
||||
//=> ist bisschen willkürlich... da natürlich auch eine falsche achse stabil sein kann. meist ist es dann genau die hälfte.
|
||||
public double getBestSingleAxis(){
|
||||
|
||||
mBpmHistory_X.removeOutliers();
|
||||
mBpmHistory_Y.removeOutliers();
|
||||
mBpmHistory_Z.removeOutliers();
|
||||
mBpmHistory_Mag.removeOutliers();
|
||||
|
||||
double varX = mBpmHistory_X.getVariance();
|
||||
double varY = mBpmHistory_Y.getVariance();
|
||||
double varZ = mBpmHistory_Z.getVariance();
|
||||
double varM = mBpmHistory_Mag.getVariance();
|
||||
double mean;
|
||||
|
||||
|
||||
//TODO: nimm den der am wenigsten streut und der, der sich am wenigsten von der letzten messung unterscheidet.
|
||||
//TODO: gewichte bpm schätzung von größeren fenstern höher.
|
||||
|
||||
if(varX < varY && varX < varZ && varX < varM){
|
||||
|
||||
mean = mBpmHistory_X.getMean();
|
||||
} else if (varY < varZ && varY < varM) {
|
||||
|
||||
mean = mBpmHistory_Y.getMean();
|
||||
} else if (varZ < varM){
|
||||
|
||||
mean = mBpmHistory_Z.getMean();
|
||||
} else {
|
||||
|
||||
mean = mBpmHistory_Mag.getMean();
|
||||
}
|
||||
|
||||
//todo: this is so ugly... needs to be refactored in app.
|
||||
mBpmHistory_X.clear();
|
||||
mBpmHistory_Y.clear();
|
||||
mBpmHistory_Z.clear();
|
||||
mBpmHistory_Mag.clear();
|
||||
|
||||
return mean;
|
||||
}
|
||||
|
||||
|
||||
public double getAverageOfAllWindows(){
|
||||
|
||||
//remove outliers solo
|
||||
//mBpmHistory_X.removeOutliers();
|
||||
//mBpmHistory_Y.removeOutliers();
|
||||
//mBpmHistory_Z.removeOutliers();
|
||||
//mBpmHistory_Mag.removeOutliers();
|
||||
|
||||
//write everything in a single vector
|
||||
BpmHistory tmpHistory = new BpmHistory();
|
||||
tmpHistory.add(mBpmHistory_X);
|
||||
tmpHistory.add(mBpmHistory_Y);
|
||||
tmpHistory.add(mBpmHistory_Z);
|
||||
tmpHistory.add(mBpmHistory_Mag);
|
||||
|
||||
if(!tmpHistory.isEmpty()){
|
||||
//remove outliers again
|
||||
tmpHistory.removeOutliers();
|
||||
|
||||
//clear
|
||||
mBpmHistory_X.clear();
|
||||
mBpmHistory_Y.clear();
|
||||
mBpmHistory_Z.clear();
|
||||
mBpmHistory_Mag.clear();
|
||||
mBpmHistory_Dist.clear();
|
||||
|
||||
return tmpHistory.getMean();
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//TODO: die einzelnen achsen cleverer kombinieren. bspw. bei Peter brauchen wir zwei Achsen in einer Bewegung.
|
||||
//TODO: Vielleicht die einzelnen Kombinationen / Magnitudes der Achsen noch mit einbeziehen. Also xy, xz, yz
|
||||
private double getBestBpmEstimation(Peaks peaksX, Peaks peaksY, Peaks peaksZ, Peaks peaksMag) throws IllegalArgumentException {
|
||||
|
||||
int cntNumAxis = 0;
|
||||
double sumCorr = 0; //to prevent division by zero
|
||||
@@ -151,6 +357,19 @@ public class BpmEstimator {
|
||||
sumNumInter += corrNumInterZ;
|
||||
}
|
||||
|
||||
// double corrMeanMag = 0, corrRmsMag = 0;
|
||||
// int corrNumInterMag = 0;
|
||||
// if(peaksMag.hasPeaks()){
|
||||
// corrMeanMag = Utils.geometricMean(peaksMag.getPeaksValueWithoutZeroIdxAndNegativeValues());
|
||||
// corrRmsMag = Utils.rms(peaksMag.getPeaksValueWithoutZeroIdx());
|
||||
// corrNumInterMag = Utils.intersectionNumber(peaksMag.getData(), 0.2f);
|
||||
//
|
||||
// ++cntNumAxis;
|
||||
// sumCorr += corrMeanMag;
|
||||
// sumRms += corrRmsMag;
|
||||
// sumNumInter += corrNumInterMag;
|
||||
// }
|
||||
|
||||
//no peaks, reject
|
||||
if(cntNumAxis == 0){
|
||||
//throw new IllegalArgumentException("All Peaks are empty! -> Reject Estimation");
|
||||
@@ -173,23 +392,28 @@ public class BpmEstimator {
|
||||
|
||||
//values to low, reject
|
||||
//TODO: this is a pretty simple assumption. first shot!
|
||||
if(corrRmsX < 0.25 && corrRmsY < 0.25 && corrRmsZ < 0.25){
|
||||
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;
|
||||
//double quantityMag = ((corrMeanMag / sumCorr) + (corrRmsMag / sumRms) + (corrNumInterMag / sumNumInter)) / cntNumAxis;
|
||||
|
||||
//get best axis by quantity and estimate bpm
|
||||
if(quantityX > quantityY && quantityX > quantityZ){
|
||||
return mBpmHistory_X.getLast();
|
||||
}
|
||||
else if(quantityY > quantityZ){
|
||||
else if(quantityY > quantityZ ){
|
||||
return mBpmHistory_Y.getLast();
|
||||
}
|
||||
else {
|
||||
return mBpmHistory_Z.getLast();
|
||||
}
|
||||
}
|
||||
|
||||
public void closeDebugWindows(){
|
||||
plotter.close();
|
||||
}
|
||||
}
|
||||
|
||||
57
java/src/main/java/bpmEstimation/BpmHistory.java
Normal file
57
java/src/main/java/bpmEstimation/BpmHistory.java
Normal file
@@ -0,0 +1,57 @@
|
||||
package bpmEstimation;
|
||||
|
||||
import utilities.Utils;
|
||||
|
||||
import java.util.LinkedList;
|
||||
|
||||
/**
|
||||
* Created by toni on 03/08/18.
|
||||
*/
|
||||
public class BpmHistory extends LinkedList<Double> {
|
||||
|
||||
private double mMean = 0.0;
|
||||
private double mVar = 0.0;
|
||||
private double mStdDev = 0.0;
|
||||
|
||||
public boolean add(double val){
|
||||
if(val != -1){
|
||||
super.add(val);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean add(BpmHistory vals){
|
||||
if(!vals.isEmpty()){
|
||||
for(double val : vals){
|
||||
super.add(val);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public double getMean(){
|
||||
if(this.size() > 2){
|
||||
return Utils.mean(this);
|
||||
} else {
|
||||
return this.getFirst(); //TODO: das ist natürlich quatsch und faulheit. mal schaun wie man das am besten löst.
|
||||
}
|
||||
}
|
||||
|
||||
public double getVariance(){
|
||||
if(this.size() > 2){
|
||||
return Utils.var(this);
|
||||
} else {
|
||||
return 0; //TODO: das ist natürlich quatsch und faulheit. mal schaun wie man das am besten löst.
|
||||
}
|
||||
}
|
||||
|
||||
public double getStd(){
|
||||
return Utils.stdDev(this);
|
||||
}
|
||||
|
||||
public void removeOutliers(){
|
||||
Utils.removeOutliersZScore(this, 3.4);
|
||||
}
|
||||
}
|
||||
67
java/src/main/java/bpmEstimation/DistanceCorrelation.java
Normal file
67
java/src/main/java/bpmEstimation/DistanceCorrelation.java
Normal file
@@ -0,0 +1,67 @@
|
||||
package bpmEstimation;
|
||||
|
||||
import utilities.Utils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.DoubleSummaryStatistics;
|
||||
|
||||
/**
|
||||
* Created by toni on 06/12/18.
|
||||
*/
|
||||
public class DistanceCorrelation {
|
||||
|
||||
//TODO: remove bad peaks found at the very beginning and end of the signal
|
||||
|
||||
private static int mMaxLag;
|
||||
private double[] mCorr;
|
||||
|
||||
public DistanceCorrelation(AccelerometerInterpolator data, int maxLag){
|
||||
|
||||
mMaxLag = maxLag;
|
||||
mCorr = calc(data);
|
||||
}
|
||||
|
||||
public double[] getCorr(){
|
||||
return mCorr;
|
||||
}
|
||||
|
||||
private double[] calc(AccelerometerInterpolator data){
|
||||
|
||||
if(mMaxLag < 1){
|
||||
throw new RuntimeException("maxlag has to be greater 1");
|
||||
}
|
||||
|
||||
//init
|
||||
int n = data.size();
|
||||
int lag_size = Math.min(mMaxLag, n - 1);
|
||||
double[] corr = new double[lag_size + 1];
|
||||
|
||||
//do the math
|
||||
for(int j = 0; j <= lag_size; ++j){
|
||||
double[] dist = new double[n - Math.abs(j)];
|
||||
int idx = 0;
|
||||
|
||||
for (int i = j; i < n; ++i){
|
||||
dist[idx] = Utils.getDistance(data.getX()[i], data.getY()[i], data.getZ()[i], data.getX()[i-j], data.getY()[i-j], data.getZ()[i-j]);
|
||||
++idx;
|
||||
}
|
||||
|
||||
corr[j] = Utils.geometricMeanLog(dist);
|
||||
}
|
||||
|
||||
//to [0, 1]
|
||||
DoubleSummaryStatistics corrStat = Arrays.stream(corr).summaryStatistics();
|
||||
double corMaxVal = corrStat.getMax();
|
||||
for(int k = 0; k < corr.length; ++k){
|
||||
corr[k] = ((corr[k] * (-1)) / corMaxVal) + 1;
|
||||
}
|
||||
|
||||
// mirror corr(2:512) and put it in front
|
||||
double[] output = new double[(2 * lag_size) + 1];
|
||||
System.arraycopy(corr, 0, output, lag_size, lag_size + 1); // +1 to place the 1.0 in the middle of correlation
|
||||
Utils.reverse(corr);
|
||||
System.arraycopy(corr, 0, output, 0, lag_size);
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
@@ -125,6 +125,7 @@ public class Peaks {
|
||||
*/
|
||||
public double getBPM(double sampleRate_ms){
|
||||
|
||||
|
||||
//todo: rückweisungsklasse kann auch hier mit rein.
|
||||
if(hasPeaks()){
|
||||
|
||||
@@ -148,6 +149,19 @@ public class Peaks {
|
||||
|
||||
mPeaksValue.add(mData[idx]);
|
||||
}
|
||||
|
||||
/*
|
||||
if(hasPeaks()) {
|
||||
//wir entfernen den ersten und den letzten peak weil die dist correlation
|
||||
//am anfang und ende oft peaks erkennt, die käse sind und so den fehler in die höhe
|
||||
//treiben können...
|
||||
mPeaksPos.removeFirst();
|
||||
mPeaksIdx.removeFirst();
|
||||
mPeaksValue.removeFirst();
|
||||
mPeaksPos.removeLast();
|
||||
mPeaksIdx.removeLast();
|
||||
mPeaksValue.removeLast();
|
||||
}*/
|
||||
}
|
||||
|
||||
//TODO: findPeaks method identical to Matlab... with PeakProminence
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
package utilities;
|
||||
|
||||
import java.awt.BasicStroke;
|
||||
import java.awt.Color;
|
||||
import java.awt.Font;
|
||||
@@ -1,21 +1,60 @@
|
||||
package utilities;
|
||||
|
||||
import bpmEstimation.AccelerometerInterpolator;
|
||||
import bpmEstimation.AccelerometerWindowBuffer;
|
||||
import bpmEstimation.Peaks;
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.text.Collator;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedList;
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
//TODO: change from double to generic type
|
||||
public class Utils {
|
||||
|
||||
public static final boolean DEBUG_MODE = false;
|
||||
|
||||
public static double getDistance(double x1, double y1, double x2, double y2) {
|
||||
return (double) Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
|
||||
}
|
||||
|
||||
public static double getDistance(double x1, double y1, double z1, double x2, double y2, double z2) {
|
||||
return (double) Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2) + (z1 - z2) * (z1 - z2));
|
||||
}
|
||||
|
||||
public static void reverse(double[] array) {
|
||||
if (array == null) {
|
||||
return;
|
||||
}
|
||||
int i = 0;
|
||||
int j = array.length - 1;
|
||||
double tmp;
|
||||
while (j > i) {
|
||||
tmp = array[j];
|
||||
array[j] = array[i];
|
||||
array[i] = tmp;
|
||||
j--;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
public static double var(LinkedList<Double> data){
|
||||
double mean = mean(data);
|
||||
double temp = 0;
|
||||
for(double a : data)
|
||||
temp += (a-mean)*(a-mean);
|
||||
return temp/(data.size()-1);
|
||||
}
|
||||
|
||||
public static double stdDev(LinkedList<Double> data){
|
||||
return Math.sqrt(var(data));
|
||||
}
|
||||
|
||||
public static double sqr(double x) {
|
||||
return x * x;
|
||||
}
|
||||
@@ -60,7 +99,7 @@ public class Utils {
|
||||
double[] diff = new double[data.length - 1];
|
||||
int i=0;
|
||||
for(int j = 1; j < data.length; ++j){
|
||||
diff[i] = data[j] - data[i];
|
||||
diff[i] = Math.abs(data[j] - data[i]);
|
||||
++i;
|
||||
}
|
||||
return diff;
|
||||
@@ -88,7 +127,7 @@ public class Utils {
|
||||
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;
|
||||
@@ -100,6 +139,18 @@ public class Utils {
|
||||
return median;
|
||||
}
|
||||
|
||||
public static double mad(List<Double> data){
|
||||
|
||||
double median = median(data);
|
||||
|
||||
java.util.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];
|
||||
@@ -109,6 +160,17 @@ public class Utils {
|
||||
return Math.pow(sum, 1.0 / data.length);
|
||||
}
|
||||
|
||||
public static double geometricMeanLog(double[] data){
|
||||
double GM_log = 0.0d;
|
||||
for (int i = 0; i < data.length; ++i) {
|
||||
if (data[i] == 0.0d) {
|
||||
return 0.0d;
|
||||
}
|
||||
GM_log += Math.log(data[i]);
|
||||
}
|
||||
return Math.exp(GM_log / data.length);
|
||||
}
|
||||
|
||||
public static int intersectionNumber(double[] signal, double border){
|
||||
int cnt = 0;
|
||||
boolean isSmallerValue = false;
|
||||
@@ -164,6 +226,51 @@ public class Utils {
|
||||
return newArray;
|
||||
}
|
||||
|
||||
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(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static double magnitude(double x, double y, double z){
|
||||
return Math.sqrt((x*x) + (y*y) + (z*z));
|
||||
}
|
||||
|
||||
public static double[] magnitude(double[] arrayX, double[] arrayY, double[] arrayZ){
|
||||
|
||||
//check of all arrays have the same size, if not crop to smallest one
|
||||
int n = arrayZ.length;
|
||||
if(arrayX.length < arrayY.length && arrayX.length < arrayZ.length){
|
||||
n = arrayX.length;
|
||||
} else if (arrayY.length < arrayZ.length){
|
||||
n = arrayY.length;
|
||||
}
|
||||
|
||||
double[] output = new double[n];
|
||||
for(int i = 0; i < n; ++i){
|
||||
output[i] = magnitude(arrayX[i], arrayY[i], arrayZ[i]);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
public static double[] magnitude(AccelerometerInterpolator interpolator){
|
||||
return magnitude(interpolator.getX(), interpolator.getY(), interpolator.getZ());
|
||||
}
|
||||
|
||||
//TODO: this could be added to AccelerometerData.. saves some time, better design.
|
||||
public static double[] magnitude(AccelerometerWindowBuffer buffer){
|
||||
return magnitude(buffer.getX(), buffer.getY(), buffer.getZ());
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public static class ShowPNG extends JFrame
|
||||
{
|
||||
@@ -178,7 +285,55 @@ public class Utils {
|
||||
this.setVisible(true);
|
||||
}
|
||||
|
||||
public void set(BufferedImage bi){
|
||||
public void plotData(String title, String name, long[] ts, double[] data){
|
||||
|
||||
double[] dTs = IntStream.range(0, ts.length).mapToDouble(i -> ts[i]).toArray();
|
||||
Plot plot = Plot.plot(Plot.plotOpts().
|
||||
title(title).
|
||||
legend(Plot.LegendFormat.BOTTOM)).
|
||||
series(name, Plot.data().xy(dTs, data), Plot.seriesOpts().color(Color.RED));
|
||||
|
||||
this.set(plot.draw());
|
||||
}
|
||||
|
||||
public void plotCorr(String title, String name, double[] data, int scale){
|
||||
|
||||
int[] tmp = IntStream.rangeClosed(-((data.length - 1)/scale), ((data.length - 1)/scale)).toArray();
|
||||
double[] range = IntStream.range(0, tmp.length).mapToDouble(i -> tmp[i]).toArray();
|
||||
Plot plot = Plot.plot(Plot.plotOpts().
|
||||
title(title).
|
||||
legend(Plot.LegendFormat.BOTTOM)).
|
||||
series(name, Plot.data().xy(range, data), Plot.seriesOpts().color(Color.RED));
|
||||
|
||||
this.set(plot.draw());
|
||||
}
|
||||
|
||||
public void plotCorrWithPeaks(String title, String name, double[] corr, int scale, Peaks peaks, boolean isCorrelation){
|
||||
|
||||
int[] tmp = IntStream.rangeClosed(-((corr.length - 1)/scale), ((corr.length - 1)/scale)).toArray();
|
||||
double[] range= IntStream.range(0, tmp.length).mapToDouble(i -> tmp[i]).toArray();
|
||||
|
||||
//if we have a correlation, shift the peaks into negative by half the correlation size
|
||||
int tmpOffset = 0;
|
||||
if(isCorrelation){
|
||||
tmpOffset = corr.length / 2;
|
||||
}
|
||||
int offset = tmpOffset;
|
||||
|
||||
LinkedList<Integer> peaksZ = peaks.getPeaksIdx();
|
||||
double[] dPeaksZX = IntStream.range(0, peaksZ.size()).mapToDouble(i -> (peaksZ.get(i) - offset)).toArray();//peaks.stream().mapToDouble(i->i).toArray();
|
||||
double[] dPeaksZY = IntStream.range(0, peaksZ.size()).mapToDouble(i -> (corr[peaksZ.get(i)])).toArray();
|
||||
Plot plot = Plot.plot(Plot.plotOpts().
|
||||
title(title).
|
||||
legend(Plot.LegendFormat.BOTTOM)).
|
||||
series(name, Plot.data().xy(range, corr), Plot.seriesOpts().color(Color.RED)).
|
||||
series("Peaks", Plot.data().xy(dPeaksZX, dPeaksZY), Plot.seriesOpts().color(Color.CYAN).
|
||||
marker(Plot.Marker.DIAMOND).line(Plot.Line.NONE));
|
||||
|
||||
this.set(plot.draw());
|
||||
}
|
||||
|
||||
private void set(BufferedImage bi){
|
||||
|
||||
mIcon = new ImageIcon(bi);
|
||||
mLabel.setVisible(false);
|
||||
@@ -187,4 +342,165 @@ public class Utils {
|
||||
}
|
||||
}
|
||||
|
||||
public static class DebugPlotter {
|
||||
|
||||
//what we want to draw
|
||||
private Utils.ShowPNG plotRawX;
|
||||
private Utils.ShowPNG plotRawY;
|
||||
private Utils.ShowPNG plotRawZ;
|
||||
private Utils.ShowPNG plotRawMag;
|
||||
private Utils.ShowPNG plotButterX;
|
||||
private Utils.ShowPNG plotButterY;
|
||||
private Utils.ShowPNG plotButterZ;
|
||||
private Utils.ShowPNG plotButterMag;
|
||||
private Utils.ShowPNG plotCorrX;
|
||||
private Utils.ShowPNG plotCorrY;
|
||||
private Utils.ShowPNG plotCorrZ;
|
||||
private Utils.ShowPNG plotCorrMag;
|
||||
private Utils.ShowPNG plotCorr3D;
|
||||
|
||||
public DebugPlotter(){
|
||||
|
||||
plotRawX = new Utils.ShowPNG();
|
||||
plotRawY = new Utils.ShowPNG();
|
||||
plotRawZ = new Utils.ShowPNG();
|
||||
plotRawMag = new Utils.ShowPNG();
|
||||
plotButterX = new Utils.ShowPNG();
|
||||
plotButterY = new Utils.ShowPNG();
|
||||
plotButterZ = new Utils.ShowPNG();
|
||||
plotButterMag = new Utils.ShowPNG();
|
||||
plotCorrX = new Utils.ShowPNG();
|
||||
plotCorrY = new Utils.ShowPNG();
|
||||
plotCorrZ = new Utils.ShowPNG();
|
||||
plotCorrMag = new Utils.ShowPNG();
|
||||
plotCorr3D = new Utils.ShowPNG();
|
||||
}
|
||||
|
||||
public void setPlotRawX(long[] ts, double[] data){
|
||||
plotRawX.plotData("Raw Data X", "x", ts, data);
|
||||
}
|
||||
|
||||
public void setPlotRawY(long[] ts, double[] data){
|
||||
plotRawY.plotData("Raw Data Y", "y", ts, data);
|
||||
}
|
||||
|
||||
public void setPlotRawZ(long[] ts, double[] data){
|
||||
plotRawZ.plotData("Raw Data Z", "z", ts, data);
|
||||
}
|
||||
|
||||
public void setPlotRawMag(long[] ts, double[] data){plotRawMag.plotData("Raw Data Magnitude", "mag", ts, data);
|
||||
}
|
||||
|
||||
public void setPlotButterX(long[] ts, double[] data){
|
||||
plotButterX.plotData("Butter Data X", "x", ts, data);
|
||||
}
|
||||
|
||||
public void setPlotButterY(long[] ts, double[] data){
|
||||
plotButterY.plotData("Butter Data Y", "y", ts, data);
|
||||
}
|
||||
|
||||
public void setPlotButterZ(long[] ts, double[] data){
|
||||
plotButterZ.plotData("Butter Data Z", "z", ts, data);
|
||||
}
|
||||
|
||||
public void setPlotButterMag(long[] ts, double[] data){
|
||||
plotButterMag.plotData("Butter Data Mag", "mag", ts, data);
|
||||
}
|
||||
|
||||
public void setPlotCorrX(double[] data, Peaks peaks){
|
||||
plotCorrX.plotCorrWithPeaks("Autocorr X", "x", data,2, peaks, true);
|
||||
}
|
||||
|
||||
public void setPlotCorrY(double[] data, Peaks peaks){
|
||||
plotCorrY.plotCorrWithPeaks("Autocorr Y", "y", data,2, peaks, true);
|
||||
}
|
||||
|
||||
public void setPlotCorrZ(double[] data, Peaks peaks){
|
||||
plotCorrZ.plotCorrWithPeaks("Autocorr Z", "z", data,2, peaks, true);
|
||||
}
|
||||
|
||||
public void setPlotCorrMag(double[] data, Peaks peaks){
|
||||
plotCorrMag.plotCorrWithPeaks("Autocorr Mag", "mag", data,2, peaks, true);
|
||||
}
|
||||
|
||||
public void setPlotCorr3D(double[] data, Peaks peaks){
|
||||
plotCorr3D.plotCorrWithPeaks("DistCorr - 3D", "3D", data,2, peaks, true);
|
||||
};
|
||||
|
||||
public void close(){
|
||||
plotRawX.dispatchEvent(new WindowEvent(plotRawX, WindowEvent.WINDOW_CLOSING));
|
||||
plotRawY.dispatchEvent(new WindowEvent(plotRawY, WindowEvent.WINDOW_CLOSING));
|
||||
plotRawZ.dispatchEvent(new WindowEvent(plotRawZ, WindowEvent.WINDOW_CLOSING));
|
||||
plotRawMag.dispatchEvent(new WindowEvent(plotRawMag, WindowEvent.WINDOW_CLOSING));
|
||||
plotButterX.dispatchEvent(new WindowEvent(plotButterX, WindowEvent.WINDOW_CLOSING));
|
||||
plotButterY.dispatchEvent(new WindowEvent(plotButterY, WindowEvent.WINDOW_CLOSING));
|
||||
plotButterZ.dispatchEvent(new WindowEvent(plotButterZ, WindowEvent.WINDOW_CLOSING));
|
||||
plotButterMag.dispatchEvent(new WindowEvent(plotButterMag, WindowEvent.WINDOW_CLOSING));
|
||||
plotCorrX.dispatchEvent(new WindowEvent(plotCorrX, WindowEvent.WINDOW_CLOSING));
|
||||
plotCorrY.dispatchEvent(new WindowEvent(plotCorrY, WindowEvent.WINDOW_CLOSING));
|
||||
plotCorrZ.dispatchEvent(new WindowEvent(plotCorrZ, WindowEvent.WINDOW_CLOSING));
|
||||
plotCorrMag.dispatchEvent(new WindowEvent(plotCorrMag, WindowEvent.WINDOW_CLOSING));
|
||||
plotCorr3D.dispatchEvent(new WindowEvent(plotCorr3D, WindowEvent.WINDOW_CLOSING));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class GroundTruthData {
|
||||
|
||||
public static class GroundTruthValue {
|
||||
public Calendar timestamp;
|
||||
public double bpm;
|
||||
|
||||
public GroundTruthValue(double bpm, Calendar ts){
|
||||
this.timestamp = ts;
|
||||
this.bpm = bpm;
|
||||
}
|
||||
}
|
||||
|
||||
private DateFormat format = new SimpleDateFormat("HH:mm:ss.SSS");
|
||||
private Vector<GroundTruthValue> gtData;
|
||||
private boolean isFile = true;
|
||||
|
||||
public GroundTruthData(){
|
||||
this.gtData = new Vector<>();
|
||||
}
|
||||
|
||||
public void setValuesFromString(String val) throws ParseException {
|
||||
|
||||
String[] measurement = val.split(" ");
|
||||
if(measurement.length < 2){
|
||||
throw new RuntimeException("broken Ground Truth format");
|
||||
}
|
||||
|
||||
double bpm = Double.valueOf(measurement[0]);
|
||||
|
||||
Calendar date = Calendar.getInstance();
|
||||
date.setTime(format.parse(measurement[1]));
|
||||
|
||||
this.gtData.add(new GroundTruthValue(bpm, date));
|
||||
}
|
||||
|
||||
public void setSingleBPM(double bpm){
|
||||
isFile = true;
|
||||
this.gtData.add(new GroundTruthValue(bpm, Calendar.getInstance()));
|
||||
}
|
||||
|
||||
public double getBPM(int idx){
|
||||
if(isFile) {
|
||||
return this.gtData.get(idx).bpm;
|
||||
} else return this.gtData.get(0).bpm;
|
||||
}
|
||||
|
||||
public long getTimestamp(int idx){
|
||||
if(isFile) {
|
||||
return 1000 * (60 * this.gtData.get(idx).timestamp.get(Calendar.MINUTE) + this.gtData.get(idx).timestamp.get(Calendar.SECOND)) + this.gtData.get(idx).timestamp.get(Calendar.MILLISECOND);
|
||||
} else return 0L;
|
||||
}
|
||||
|
||||
public int getSize(){
|
||||
return this.gtData.size();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,58 +1,47 @@
|
||||
%We are using a threshold-based version for bpm estimation
|
||||
%Only the z axis of the acc is used
|
||||
%using autocorrelation to estimate the current bmp within some fixed window
|
||||
|
||||
%NOTE: depending on the measurement device we have a highly different sample rate. the smartwatches are not capable of providing a constant sample rate. the xsens on the other hand is able to do this.
|
||||
|
||||
%load file provided by the sensor readout app
|
||||
|
||||
% SMARTWATCH LG WEAR ------> 100 hz - 1000hz
|
||||
%measurements = dlmread('../../measurements/lgWear/PR_recording_80bpm_4-4_177596720.csv', ';'); %
|
||||
%measurements = dlmread('../../measurements/lgWear/recording_48bpm_4-4_176527527.csv', ';');
|
||||
%measurements = dlmread('../../measurements/lgWear/recording_48bpm_4-4_176606785.csv', ';');
|
||||
%measurements = dlmread('../../measurements/lgWear/recording_48bpm_4-4_176696356.csv', ';');
|
||||
%measurements = dlmread('../../measurements/lgWear/recording_48bpm_4-4_176820066.csv', ';'); %
|
||||
%measurements = dlmread('../../measurements/lgWear/recording_48bpm_4-4_176931941.csv', ';'); %double
|
||||
%measurements = dlmread('../../measurements/lgWear/recording_72bpm_4-4_176381633.csv', ';');
|
||||
%measurements = dlmread('../../measurements/lgWear/recording_72bpm_4-4_176453327.csv', ';'); %
|
||||
%measurements = dlmread('../../measurements/lgWear/recording_100bpm_4-4_176073767.csv', ';'); %*
|
||||
%measurements = dlmread('../../measurements/lgWear/recording_100bpm_4-4_176165357.csv', ';');
|
||||
%measurements = dlmread('../../measurements/lgWear/recording_100bpm_4-4_176230146.csv', ';');
|
||||
%measurements = dlmread('../../measurements/lgWear/recording_100bpm_4-4_176284687.csv', ';'); %
|
||||
%measurements = dlmread('../../measurements/lgWear/recording_100bpm_4-4_177368860.csv', ';'); %(besonders)
|
||||
%measurements = dlmread('../../measurements/lgWear/recording_180bpm_4-4_177011641.csv', ';'); %
|
||||
%measurements = dlmread('../../measurements/lgWear/recording_180bpm_4-4_177064915.csv', ';'); %
|
||||
|
||||
|
||||
% SMARTWATCH G WATCH WEAR R ----> 100hz - 250hz
|
||||
%measurements = dlmread('../measurements/wearR/PR_recording_80bpm_4-4_177596720.csv', ';'); %*
|
||||
%measurements = dlmread('../measurements/wearR/recording_48bpm_4-4_176527527.csv', ';');
|
||||
%measurements = dlmread('../measurements/wearR/recording_48bpm_4-4_176606785.csv', ';');
|
||||
%measurements = dlmread('../../measurements/wearR/recording_48bpm_4-4_176696356.csv', ';'); %*
|
||||
%measurements = dlmread('../measurements/wearR/recording_48bpm_4-4_176820066.csv', ';');
|
||||
%measurements = dlmread('../measurements/wearR/recording_48bpm_4-4_176931941.csv', ';'); %double
|
||||
%measurements = dlmread('../measurements/wearR/recording_72bpm_4-4_176381633.csv', ';');
|
||||
%measurements = dlmread('../measurements/wearR/recording_72bpm_4-4_176453327.csv', ';');
|
||||
%measurements = dlmread('../measurements/wearR/recording_100bpm_4-4_176073767.csv', ';'); *
|
||||
%measurements = dlmread('../measurements/wearR/recording_100bpm_4-4_176165357.csv', ';');
|
||||
%measurements = dlmread('../measurements/wearR/recording_100bpm_4-4_176230146.csv', ';'); %* 72?
|
||||
%measurements = dlmread('../measurements/wearR/recording_100bpm_4-4_176284687.csv', ';'); %*48?
|
||||
%measurements = dlmread('../measurements/wearR/recording_100bpm_4-4_177368860.csv', ';'); %(besonders)
|
||||
%measurements = dlmread('../measurements/wearR/recording_180bpm_4-4_177011641.csv', ';'); *
|
||||
%measurements = dlmread('../measurements/wearR/recording_180bpm_4-4_177064915.csv', ';'); *
|
||||
|
||||
|
||||
files = dir(fullfile('../../measurements/lgWear/', '*.csv'));
|
||||
%load sensor files
|
||||
%files = dir(fullfile('../../measurements/2017.06/mSensor/', '*.csv'));
|
||||
%files = natsortfiles(dir(fullfile('../../measurements/2017.06/lgWear/', '*.csv')));
|
||||
%files = dir(fullfile('../../measurements/wearR/', '*.csv'));
|
||||
%files = dir(fullfile('../../measurements/peter_failed/', '*.csv'));
|
||||
%files = dir(fullfile('../../measurements/2018.06/manfred/LGWatchR/', '*.csv'));
|
||||
%files = dir(fullfile('../../measurements/2018.06/peter/Huawai/', '*.csv'));
|
||||
%files = dir(fullfile('../../measurements/2018.06/peter/mSensor/', '*.csv'));
|
||||
files = dir(fullfile('../../measurements/2018.06/frank/mSensorTest/', '*.csv'));
|
||||
%files = dir(fullfile('../../measurements/2018.06/leon/mSensor/', '*.csv'));
|
||||
|
||||
%files_sorted = natsortfiles({files.name});
|
||||
for file = files'
|
||||
|
||||
filename = [file.folder '/' file.name];
|
||||
measurements = dlmread(filename, ';');
|
||||
measurements = dlmread(filename, ';', 3, 0);
|
||||
|
||||
%load ground truth file
|
||||
fid = fopen(filename);
|
||||
fgetl(fid);
|
||||
Str = fgetl(fid);
|
||||
Key = 'Metronom: ';
|
||||
Index = strfind(Str, Key);
|
||||
gtDataRaw = sscanf(Str(Index(1) + length(Key):end), '%g', 1);
|
||||
gtData = [];
|
||||
gtFile = [];
|
||||
if(isempty(gtDataRaw))
|
||||
gtFile = extractAfter(Str, Key);
|
||||
gtFile = strcat('../../measurements/2018.06/gt_toni/', gtFile);
|
||||
f = fopen(gtFile);
|
||||
gtDataRaw = textscan(f, '%f %s', 'Delimiter', ' ');
|
||||
fclose(f);
|
||||
[~,~,~,hours,minutes,seconds] = datevec(gtDataRaw{2}, 'HH:MM:SS.FFF');
|
||||
gtData(:,1) = 1000*(60*minutes + seconds); %we do not use hours!
|
||||
gtData(:,2) = gtDataRaw{1};
|
||||
else
|
||||
gtData = gtDataRaw;
|
||||
end
|
||||
|
||||
%draw the raw acc data
|
||||
m_idx = [];
|
||||
m_idx = (measurements(:,2)==2); %Android App: 10, Normal Data: 2
|
||||
m_idx = (measurements(:,2)==3); %Android App: 10, Sensor: 3, Normal Data: 2
|
||||
m = measurements(m_idx, :);
|
||||
|
||||
%Interpolate to generate a constant sample rate to 250hz (4ms per sample)
|
||||
@@ -66,53 +55,76 @@ for file = files'
|
||||
%put all together again
|
||||
m = [t_interp', t_interp', m_interp];
|
||||
|
||||
figure(1);
|
||||
plot(m(:,1),m(:,3)) %x
|
||||
legend("x", "location", "eastoutside");
|
||||
|
||||
figure(2);
|
||||
plot(m(:,1),m(:,4)) %yt
|
||||
legend("y", "location", "eastoutside");
|
||||
|
||||
figure(3);
|
||||
plot(m(:,1),m(:,5)) %z
|
||||
legend("z", "location", "eastoutside");
|
||||
% figure(1);
|
||||
% plot(m(:,1),m(:,3)) %x
|
||||
% legend("x", "location", "eastoutside");
|
||||
%
|
||||
% figure(2);
|
||||
% 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");
|
||||
|
||||
%magnitude
|
||||
magnitude = sqrt(sum(m(:,3:5).^2,2));
|
||||
figure(5);
|
||||
plot(m(:,1), magnitude);
|
||||
legend("magnitude", "location", "eastoutside");
|
||||
|
||||
waitforbuttonpress();
|
||||
%waitforbuttonpress();
|
||||
|
||||
%save timestamps
|
||||
timestamps = m(:,1);
|
||||
data = m(:,3); %only z
|
||||
|
||||
%TODO: Different window sizes for periods under 16.3 s
|
||||
window_size = 2048; %about 2 seconds using 2000hz, 16.3 s using 250hz
|
||||
window_size = 1024; %about 2 seconds using 2000hz, 16.3 s using 250hz
|
||||
overlap = 256;
|
||||
bpm_per_window_ms = [];
|
||||
bpm_per_window = [];
|
||||
for i = window_size+1:length(data)
|
||||
bpm_3D = [];
|
||||
ms_3D = [];
|
||||
|
||||
gtIdx = 1;
|
||||
gtError_3D = [];
|
||||
gtError_1D = [];
|
||||
|
||||
for i = window_size+1:1:length(data)
|
||||
|
||||
%wait until window is filled with new data
|
||||
if(mod(i,overlap) == 0)
|
||||
|
||||
|
||||
%set cur ground truth
|
||||
if(length(gtData) > 1)
|
||||
curTimestamp = timestamps(i) - timestamps(1);
|
||||
while(curTimestamp > gtData(gtIdx,1) && gtIdx < length(gtData))
|
||||
curGtBpm = gtData(gtIdx,2);
|
||||
gtIdx = gtIdx + 1;
|
||||
end
|
||||
else
|
||||
curGtBpm = gtData;
|
||||
end
|
||||
|
||||
|
||||
%measure periodicity of window and use axis with best periodicity
|
||||
[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, lag_x] = xcov(m(i-window_size:i,3), (window_size/2), "coeff");
|
||||
[corr_y, lag_y] = xcov(m(i-window_size:i,4), (window_size/2), "coeff");
|
||||
[corr_z, lag_z] = xcov(m(i-window_size:i,5), (window_size/2), "coeff");
|
||||
|
||||
%magnitude
|
||||
[corr_mag, lag_mag] = xcov(magnitude(i-window_size:i), (window_size/2), "coeff");
|
||||
|
||||
%TODO: stichwort spatial autocorrelation
|
||||
%figure(77);
|
||||
%scatter3(timestamps(i-window_size:i), m(i-window_size:i,4), m(i-window_size:i,5));
|
||||
|
||||
%distanz zwischen den vektoren nehmen und in eine normale autocorrelation zu packen
|
||||
%aufpassen wegen der norm, dass die richtung quasi nicht verloren geht.
|
||||
%https://en.wikipedia.org/wiki/Lp_space
|
||||
[corr_3D, lag_3D] = distCorr(m(i-window_size:i, 3:5), (window_size/2));
|
||||
|
||||
corr_x_pos = corr_x;
|
||||
corr_y_pos = corr_y;
|
||||
corr_z_pos = corr_z;
|
||||
@@ -127,67 +139,82 @@ for file = files'
|
||||
[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);
|
||||
[peak_3D, idx_3D_raw] = findpeaks(corr_3D, '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_3D_raw = sort(idx_3D_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);
|
||||
%idx_3D = findFalseDetectedPeaks(idx_3D_raw, lag_3D', corr_3D);
|
||||
idx_3D = idx_3D_raw;
|
||||
|
||||
Dwindow = m(i-window_size:i,3);
|
||||
Dwindow_mean_ts_diff = mean(diff(lag_3D(idx_3D) * sample_rate_ms)); %2.5 ms is the time between two samples at 400hz
|
||||
Dwindow_mean_bpm = (60000 / (Dwindow_mean_ts_diff));
|
||||
|
||||
% figure(10);
|
||||
% plot(lag_3D, corr_3D, lag_3D(idx_3D), corr_3D(idx_3D), 'r*', lag_3D(idx_3D_raw), corr_3D(idx_3D_raw), 'g*')
|
||||
% hold ("on")
|
||||
% m_label_ms = strcat(" mean ms: ", num2str(Dwindow_mean_ts_diff));
|
||||
% m_label_bpm = strcat(" mean bpm: ", num2str(Dwindow_mean_bpm));
|
||||
% title(strcat(" ", m_label_ms, " ", m_label_bpm));
|
||||
% hold ("off");
|
||||
|
||||
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
|
||||
Xwindow_mean_bpm = (60000 / (Xwindow_mean_ts_diff));
|
||||
|
||||
figure(11);
|
||||
plot(lag_x, corr_x, lag_x(idx_x), corr_x(idx_x), 'r*', lag_x(idx_x_raw), corr_x(idx_x_raw), 'g*') %z
|
||||
hold ("on")
|
||||
m_label_ms = strcat(" mean ms: ", num2str(Xwindow_mean_ts_diff));
|
||||
m_label_bpm = strcat(" mean bpm: ", num2str(Xwindow_mean_bpm));
|
||||
title(strcat(" ", m_label_ms, " ", m_label_bpm));
|
||||
hold ("off");
|
||||
|
||||
% figure(11);
|
||||
% plot(lag_x, corr_x, lag_x(idx_x), corr_x(idx_x), 'r*', lag_x(idx_x_raw), corr_x(idx_x_raw), 'g*') %z
|
||||
% hold ("on")
|
||||
% m_label_ms = strcat(" mean ms: ", num2str(Xwindow_mean_ts_diff));
|
||||
% m_label_bpm = strcat(" mean bpm: ", num2str(Xwindow_mean_bpm));
|
||||
% title(strcat(" ", m_label_ms, " ", m_label_bpm));
|
||||
% hold ("off");
|
||||
|
||||
Ywindow = m(i-window_size:i,4);
|
||||
Ywindow_mean_ts_diff = mean(diff(lag_y(idx_y) * sample_rate_ms));
|
||||
Ywindow_mean_bpm = (60000 / (Ywindow_mean_ts_diff));
|
||||
|
||||
figure(12);
|
||||
plot(lag_y, corr_y, lag_y(idx_y), corr_y(idx_y), 'r*', lag_y(idx_y_raw), corr_y(idx_y_raw), 'g*') %z
|
||||
hold ("on")
|
||||
m_label_ms = strcat(" mean ms: ", num2str(Ywindow_mean_ts_diff));
|
||||
m_label_bpm = strcat(" mean bpm: ", num2str(Ywindow_mean_bpm));
|
||||
title(strcat(" ", m_label_ms, " ", m_label_bpm));
|
||||
hold ("off");
|
||||
% figure(12);
|
||||
% plot(lag_y, corr_y, lag_y(idx_y), corr_y(idx_y), 'r*', lag_y(idx_y_raw), corr_y(idx_y_raw), 'g*') %z
|
||||
% hold ("on")
|
||||
% m_label_ms = strcat(" mean ms: ", num2str(Ywindow_mean_ts_diff));
|
||||
% m_label_bpm = strcat(" mean bpm: ", num2str(Ywindow_mean_bpm));
|
||||
% title(strcat(" ", m_label_ms, " ", m_label_bpm));
|
||||
% hold ("off");
|
||||
|
||||
Zwindow = m(i-window_size:i,5);
|
||||
Zwindow_mean_ts_diff = mean(diff(lag_z(idx_z)* sample_rate_ms));
|
||||
Zwindow_mean_bpm = (60000 / (Zwindow_mean_ts_diff));
|
||||
|
||||
figure(13);
|
||||
plot(lag_z, corr_z, lag_z(idx_z), corr_z(idx_z), 'r*', lag_z(idx_z_raw), corr_z(idx_z_raw), 'g*') %z
|
||||
hold ("on")
|
||||
m_label_ms = strcat(" mean ms: ", num2str(Zwindow_mean_ts_diff));
|
||||
m_label_bpm = strcat(" mean bpm: ", num2str(Zwindow_mean_bpm));
|
||||
title(strcat(" ", m_label_ms, " ", m_label_bpm));
|
||||
hold ("off");
|
||||
% figure(13);
|
||||
% plot(lag_z, corr_z, lag_z(idx_z), corr_z(idx_z), 'r*', lag_z(idx_z_raw), corr_z(idx_z_raw), 'g*') %z
|
||||
% hold ("on")
|
||||
% m_label_ms = strcat(" mean ms: ", num2str(Zwindow_mean_ts_diff));
|
||||
% m_label_bpm = strcat(" mean bpm: ", num2str(Zwindow_mean_bpm));
|
||||
% 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");
|
||||
|
||||
|
||||
% 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))
|
||||
@@ -277,11 +304,22 @@ for file = files'
|
||||
|
||||
if(isnan(window_mean_ts_diff) || isnan(window_mean_bpm))
|
||||
%do nothing
|
||||
else
|
||||
else
|
||||
gtError_1D = [gtError_1D, abs(window_mean_bpm - curGtBpm)];
|
||||
bpm_per_window_ms = [bpm_per_window_ms, window_mean_ts_diff];
|
||||
bpm_per_window = [bpm_per_window, window_mean_bpm];
|
||||
end
|
||||
|
||||
|
||||
%3D mean
|
||||
if(isnan(Dwindow_mean_bpm))
|
||||
%nothing
|
||||
else
|
||||
gtError_3D = [gtError_3D, abs(Dwindow_mean_bpm - curGtBpm)];
|
||||
bpm_3D = [bpm_3D, Dwindow_mean_bpm];
|
||||
ms_3D = [ms_3D, Dwindow_mean_ts_diff];
|
||||
end
|
||||
|
||||
%TODO: if correlation value is lower then a treshhold, we are not conducting TODO: change to a real classification instead of a treshhold.
|
||||
|
||||
end
|
||||
@@ -290,22 +328,70 @@ for file = files'
|
||||
%TODO: smooth the results using a moving avg or 1d kalman filter.(transition for kalman could be adding the last measured value)
|
||||
|
||||
%remove the first 40% of the results, due to starting delays while recording.
|
||||
number_to_remove = round(abs(0.1 * length(bpm_per_window_ms)));
|
||||
num_all = length(bpm_per_window_ms);
|
||||
bpm_per_window_ms = bpm_per_window_ms(number_to_remove:num_all);
|
||||
bpm_per_window = bpm_per_window(number_to_remove:num_all);
|
||||
%number_to_remove = round(abs(0.1 * length(bpm_per_window_ms)));
|
||||
%num_all = length(bpm_per_window_ms);
|
||||
%bpm_per_window_ms = bpm_per_window_ms(number_to_remove:num_all);
|
||||
%bpm_per_window = bpm_per_window(number_to_remove:num_all);
|
||||
|
||||
mean_final_ms = mean(bpm_per_window_ms);
|
||||
std_final_ms = std(bpm_per_window_ms);
|
||||
|
||||
mean_final_bpm = mean(bpm_per_window);
|
||||
std_final_bpm = std(bpm_per_window);
|
||||
|
||||
mean_final_error_1D = mean(gtError_1D);
|
||||
std_final_error_1D = std(gtError_1D);
|
||||
|
||||
mean_final_ms_3D = mean(ms_3D);
|
||||
std_final_ms_3D = std(ms_3D);
|
||||
|
||||
mean_final_bpm_3D = mean(bpm_3D);
|
||||
std_final_bpm_3D = std(bpm_3D);
|
||||
|
||||
mean_final_error_3D = mean(gtError_3D);
|
||||
std_final_error_3D = std(gtError_3D);
|
||||
|
||||
fprintf('%s: mean = %f bpm (%f ms) stddev = %f bpm (%f ms)\n', strrep(regexprep(filename,'^.*recording_',''),'.txt',''), mean_final_bpm, mean_final_ms, std_final_bpm, std_final_ms);
|
||||
|
||||
fprintf('%s: mean = %f bpm (%f bpm) stddev = %f bpm (%f bpm) --- 1D\n', strrep(regexprep(filename,'^.*recording_',''),'.txt',''), mean_final_error_1D, mean_final_bpm, std_final_error_1D, std_final_bpm);
|
||||
fprintf('%s: mean = %f bpm (%f bpm) stddev = %f bpm (%f bpm) --- 3D\n', strrep(regexprep(filename,'^.*recording_',''),'.txt',''), mean_final_error_3D, mean_final_bpm_3D, std_final_error_3D, std_final_bpm_3D);
|
||||
|
||||
end
|
||||
|
||||
|
||||
% %1D fft - nicht so der brüller
|
||||
% z_fft = fft(m(i-window_size:i,5));
|
||||
% L = length(z_fft);
|
||||
% Fs = 250;
|
||||
% P2 = abs(z_fft/L);
|
||||
% P1 = P2(1:L/2+1);
|
||||
% P1(2:end-1) = 2*P1(2:end-1);
|
||||
% f = Fs*(0:(L/2))/L; %nyquist frequence
|
||||
%
|
||||
% figure(66);
|
||||
% plot(f, P1);
|
||||
%
|
||||
% %3D fft
|
||||
% m_3D = m(i-window_size:i, 3);
|
||||
% m_3D(:,:,2) = m(i-window_size:i, 4);
|
||||
% m_3D(:,:,3) = m(i-window_size:i, 5);
|
||||
%
|
||||
% fft_3D = fftn(m_3D);
|
||||
%
|
||||
% %2D fft
|
||||
% fft_xy = fft2(m(i-window_size:i, 3:4));
|
||||
% fft_yz = fft2(m(i-window_size:i, 3:4));
|
||||
%
|
||||
% fft_test = fft(m(i-window_size:i, 3:5),[],2);
|
||||
%
|
||||
% figure(60);
|
||||
% imagesc(abs(fftshift(fft_test)));
|
||||
%
|
||||
% figure(61);
|
||||
% imagesc(abs(fftshift(fft_xy)));
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
41
matlab/distCorr.m
Normal file
41
matlab/distCorr.m
Normal file
@@ -0,0 +1,41 @@
|
||||
% Autocorrelation for Points based on the distance between those points
|
||||
% data is the sensor data. one point per row.
|
||||
% lag_size is the number of required lags
|
||||
function [corr, lag] = distCorr(data, lag_size)
|
||||
|
||||
n = length(data);
|
||||
|
||||
%if lag size is bigger as data, then use data length - 1
|
||||
% -1 because the max. lag is -1 of the data length..
|
||||
if(lag_size >= n)
|
||||
lag_size = n - 1; %
|
||||
end
|
||||
|
||||
%init
|
||||
corr = zeros(lag_size + 1, 1); % +1, because the first index is lag 0.
|
||||
lag = (-lag_size:1:lag_size)';
|
||||
|
||||
%mean shift and l2 normalization
|
||||
%data = data - mean(data);
|
||||
%data = data / norm(data);
|
||||
|
||||
for j = 1:lag_size + 1 % +1, because the first index is lag 0.
|
||||
dist = zeros(n - abs(j), 1);
|
||||
idx = 1;
|
||||
|
||||
for i = j:n
|
||||
%x_i * x_i-(j-1)
|
||||
dist(idx) = norm(data(i, :) - data(i-(j-1), :));
|
||||
idx = idx + 1;
|
||||
end
|
||||
|
||||
corr(j) = geomean(dist);
|
||||
end
|
||||
|
||||
%mirror corr(2:512) and put it infront
|
||||
corr = [flipud(corr(2:end)); corr];
|
||||
|
||||
%to [0, 1]
|
||||
corr = ((corr .* -1) / max(corr)) + 1;
|
||||
|
||||
end
|
||||
Reference in New Issue
Block a user