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; } }