package pt.chambino.p.pulse;
import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Typeface;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.WindowManager;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedList;
import java.util.List;
import org.opencv.android.BaseLoaderCallback;
import org.opencv.android.MyCameraBridgeViewBase;
import org.opencv.android.MyCameraBridgeViewBase.CvCameraViewListener;
import org.opencv.android.LoaderCallbackInterface;
import org.opencv.android.OpenCVLoader;
import org.opencv.core.Mat;
import org.opencv.core.Rect;
import org.opencv.highgui.Highgui;
import pt.chambino.p.pulse.Pulse.Face;
import pt.chambino.p.pulse.dialog.BpmDialog;
import pt.chambino.p.pulse.dialog.ConfigDialog;
import pt.chambino.p.pulse.view.BpmView;
import pt.chambino.p.pulse.view.PulseView;
public class App extends Activity implements CvCameraViewListener {
private static final String TAG = "Pulse::App";
private MyCameraBridgeViewBase camera;
private BpmView bpmView;
private PulseView pulseView;
private Pulse pulse;
private Paint faceBoxPaint;
private Paint faceBoxTextPaint;
private ConfigDialog configDialog;
private BaseLoaderCallback loaderCallback = new BaseLoaderCallback(this) {
@Override
public void onManagerConnected(int status) {
switch (status) {
case LoaderCallbackInterface.SUCCESS:
loaderCallbackSuccess();
break;
default:
super.onManagerConnected(status);
break;
}
}
};
private void loaderCallbackSuccess() {
System.loadLibrary("pulse");
pulse = new Pulse();
pulse.setFaceDetection(initFaceDetection);
pulse.setMagnification(initMagnification);
pulse.setMagnificationFactor(initMagnificationFactor);
File dir = getDir("cascade", Context.MODE_PRIVATE);
File file = createFileFromResource(dir, R.raw.lbpcascade_frontalface, "xml");
pulse.load(file.getAbsolutePath());
dir.delete();
pulseView.setGridSize(pulse.getMaxSignalSize());
camera.enableView();
}
private File createFileFromResource(File dir, int id, String extension) {
String name = getResources().getResourceEntryName(id) + "." + extension;
InputStream is = getResources().openRawResource(id);
File file = new File(dir, name);
try {
FileOutputStream os = new FileOutputStream(file);
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
is.close();
os.close();
} catch (IOException ex) {
Log.e(TAG, "Failed to create file: " + file.getPath(), ex);
}
return file;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
setContentView(R.layout.app);
camera = (MyCameraBridgeViewBase) findViewById(R.id.camera);
camera.setCvCameraViewListener(this);
camera.SetCaptureFormat(Highgui.CV_CAP_ANDROID_COLOR_FRAME_RGB);
camera.setMaxFrameSize(600, 600);
bpmView = (BpmView) findViewById(R.id.bpm);
bpmView.setBackgroundColor(Color.DKGRAY);
bpmView.setTextColor(Color.LTGRAY);
pulseView = (PulseView) findViewById(R.id.pulse);
faceBoxPaint = initFaceBoxPaint();
faceBoxTextPaint = initFaceBoxTextPaint();
}
private static final String CAMERA_ID = "camera-id";
private static final String FPS_METER = "fps-meter";
private static final String FACE_DETECTION = "face-detection";
private static final String MAGNIFICATION = "magnification";
private static final String MAGNIFICATION_FACTOR = "magnification-factor";
private boolean initFaceDetection = true;
private boolean initMagnification = true;
private int initMagnificationFactor = 100;
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
camera.setCameraId(savedInstanceState.getInt(CAMERA_ID));
camera.setFpsMeter(savedInstanceState.getBoolean(FPS_METER));
initFaceDetection = savedInstanceState.getBoolean(FACE_DETECTION, initFaceDetection);
initMagnification = savedInstanceState.getBoolean(MAGNIFICATION, initMagnification);
initMagnificationFactor = savedInstanceState.getInt(MAGNIFICATION_FACTOR, initMagnificationFactor);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(CAMERA_ID, camera.getCameraId());
outState.putBoolean(FPS_METER, camera.isFpsMeterEnabled());
// if OpenCV Manager is not installed, pulse hasn't loaded
if (pulse != null) {
outState.putBoolean(FACE_DETECTION, pulse.hasFaceDetection());
outState.putBoolean(MAGNIFICATION, pulse.hasMagnification());
outState.putInt(MAGNIFICATION_FACTOR, pulse.getMagnificationFactor());
}
}
@Override
public void onResume() {
super.onResume();
OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_2_4_11, this, loaderCallback);
}
@Override
public void onPause() {
if (camera != null) {
camera.disableView();
}
bpmView.setNoBpm();
pulseView.setNoPulse();
super.onPause();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.app, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.record:
onRecord(item);
return true;
case R.id.switch_camera:
camera.switchCamera();
return true;
case R.id.config:
if (configDialog == null) configDialog = new ConfigDialog();
configDialog.show(getFragmentManager(), null);
return true;
}
return super.onOptionsItemSelected(item);
}
private boolean recording = false;
private List<Double> recordedBpms;
private BpmDialog bpmDialog;
private double recordedBpmAverage;
private void onRecord(MenuItem item) {
recording = !recording;
if (recording) {
item.setIcon(android.R.drawable.ic_media_pause);
if (recordedBpms == null) recordedBpms = new LinkedList<Double>();
else recordedBpms.clear();
} else {
item.setIcon(android.R.drawable.ic_media_play);
recordedBpmAverage = 0;
for (double bpm : recordedBpms) recordedBpmAverage += bpm;
recordedBpmAverage /= recordedBpms.size();
if (bpmDialog == null) bpmDialog = new BpmDialog();
bpmDialog.show(getFragmentManager(), null);
}
}
public double getRecordedBpmAverage() {
return recordedBpmAverage;
}
public Pulse getPulse() {
return pulse;
}
public MyCameraBridgeViewBase getCamera() {
return camera;
}
private Rect noFaceRect;
private Rect initNoFaceRect(int width, int height) {
double r = pulse.getRelativeMinFaceSize();
int x = (int)(width * (1. - r) / 2.);
int y = (int)(height * (1. - r) / 2.);
int w = (int)(width * r);
int h = (int)(height * r);
return new Rect(x, y, w, h);
}
@Override
public void onCameraViewStarted(int width, int height) {
Log.d(TAG, "onCameraViewStarted("+width+", "+height+")");
pulse.start(width, height);
noFaceRect = initNoFaceRect(width, height);
}
@Override
public void onCameraViewStopped() {
}
@Override
public Mat onCameraFrame(Mat frame) {
pulse.onFrame(frame);
return frame;
}
@Override
public void onCameraFrame(Canvas canvas) {
Face face = getCurrentFace(pulse.getFaces()); // TODO support multiple faces
if (face != null) {
onFace(canvas, face);
} else {
// draw no face box
canvas.drawPath(createFaceBoxPath(noFaceRect), faceBoxPaint);
canvas.drawText("Face here",
canvas.getWidth() / 2f,
canvas.getHeight() / 2f,
faceBoxTextPaint);
// no faces
runOnUiThread(new Runnable() {
@Override
public void run() {
bpmView.setNoBpm();
pulseView.setNoPulse();
}
});
}
}
private int currentFaceId = 0;
private Face getCurrentFace(Face[] faces) {
Face face = null;
if (currentFaceId > 0) {
face = findFace(faces, currentFaceId);
}
if (face == null && faces.length > 0) {
face = faces[0];
}
if (face == null) {
currentFaceId = 0;
} else {
currentFaceId = face.getId();
}
return face;
}
private Face findFace(Face[] faces, int id) {
for (Face face : faces) {
if (face.getId() == id) {
return face;
}
}
return null;
}
private void onFace(Canvas canvas, Face face) {
// grab face box
Rect box = face.getBox();
// draw face box
canvas.drawPath(createFaceBoxPath(box), faceBoxPaint);
if (pulse.hasFaceDetection() && !face.existsPulse()) {
// draw hint text
canvas.drawText("Hold still",
box.x + box.width / 2f,
box.y + box.height / 2f - 20,
faceBoxTextPaint);
canvas.drawText("in a",
box.x + box.width / 2f,
box.y + box.height / 2f,
faceBoxTextPaint);
canvas.drawText("bright place",
box.x + box.width / 2f,
box.y + box.height / 2f + 20,
faceBoxTextPaint);
}
// update views
if (face.existsPulse()) {
final double bpm = face.getBpm();
final double[] signal = face.getPulse();
runOnUiThread(new Runnable() {
@Override
public void run() {
bpmView.setBpm(bpm);
pulseView.setPulse(signal);
}
});
if (recording) {
recordedBpms.add(bpm);
}
} else {
// no pulse
runOnUiThread(new Runnable() {
@Override
public void run() {
bpmView.setNoBpm();
pulseView.setNoPulse();
}
});
}
}
private Paint initFaceBoxPaint() {
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
p.setColor(Color.WHITE);
p.setStyle(Paint.Style.STROKE);
p.setStrokeWidth(4);
p.setStrokeCap(Paint.Cap.ROUND);
p.setStrokeJoin(Paint.Join.ROUND);
p.setShadowLayer(2, 0, 0, Color.BLACK);
return p;
}
private Paint initFaceBoxTextPaint() {
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
p.setColor(Color.WHITE);
p.setShadowLayer(2, 0, 0, Color.DKGRAY);
p.setTypeface(Typeface.createFromAsset(getAssets(), "fonts/ds_digital/DS-DIGIB.TTF"));
p.setTextAlign(Paint.Align.CENTER);
p.setTextSize(20f);
return p;
}
private Path createFaceBoxPath(Rect box) {
float size = box.width * 0.25f;
Path path = new Path();
// top left
path.moveTo(box.x, box.y + size);
path.lineTo(box.x, box.y);
path.lineTo(box.x + size, box.y);
// top right
path.moveTo(box.x + box.width, box.y + size);
path.lineTo(box.x + box.width, box.y);
path.lineTo(box.x + box.width - size, box.y);
// bottom left
path.moveTo(box.x, box.y + box.height - size);
path.lineTo(box.x, box.y + box.height);
path.lineTo(box.x + size, box.y + box.height);
// bottom right
path.moveTo(box.x + box.width, box.y + box.height - size);
path.lineTo(box.x + box.width, box.y + box.height);
path.lineTo(box.x + box.width - size, box.y + box.height);
return path;
}
}