下载了一个可以利用摄像头的心电图软件,证实确实可以用,无需其他任何传感器的辅助。虽然用起来需要一些技巧。先自己人工测算了心率大概60dpm左右,用软件测了一下,看一些截图:
有时候偏高

请输入图片描述

有时候测量值偏低
那么基本可以确定是通过摄像头采集指尖部分的图像数据,然后通过医学意义上的图像模式去匹配采集到的数据,简单的说就是一个图像处理的过程。指尖的血液循环和脉搏无非体现在图像颜色和形状的变化。上google上找到了一个简单例子的源码,供参考。
主activity

package com.jwetherell.heart_rate_monitor;

import java.util.concurrent.atomic.AtomicBoolean;

   
  import android.app.Activity;
  
import android.content.Context;
import android.content.res.Configuration;
import android.hardware.Camera;
import android.hardware.Camera.PreviewCallback;
import android.os.Bundle;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.widget.TextView;

/**
* This class extends Activity to handle a picture preview, process the preview for a red values
* and determine a heart beat.
*
* @author Justin Wetherell <[email protected]>
*/
public class HeartRateMonitor extends Activity {
private static final String TAG = "HeartRateMonitor";
private static final AtomicBoolean processing = new AtomicBoolean(false);

private static SurfaceView preview = null;
private static SurfaceHolder previewHolder = null;
private static Camera camera = null;
private static View image = null;
private static TextView text = null;

private static WakeLock wakeLock = null;

private static int averageIndex = 0;
private static final int averageArraySize = 4;
private static final int[] averageArray = new int[averageArraySize];

public static enum TYPE { GREEN, RED };
private static TYPE currentType = TYPE.GREEN;
public static TYPE getCurrent() {
return currentType;
}

private static int beatsIndex = 0;
private static final int beatsArraySize = 3;
private static final int[] beatsArray = new int[beatsArraySize];
private static double beats = 0;
private static long startTime = 0;

/**
* {@inheritDoc}
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

preview = (SurfaceView)findViewById(R.id.preview);
previewHolder = preview.getHolder();
previewHolder.addCallback(surfaceCallback);
previewHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);

image = findViewById(R.id.image);
text = (TextView) findViewById(R.id.text);

PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
wakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, "DoNotDimScreen");
}

/**
* {@inheritDoc}
*/
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}

/**
* {@inheritDoc}
*/
@Override
public void onResume() {
super.onResume();

wakeLock.acquire();

camera = Camera.open();

startTime = System.currentTimeMillis();
}

/**
* {@inheritDoc}
*/
@Override
public void onPause() {
super.onPause();

wakeLock.release();

camera.setPreviewCallback(null);
camera.stopPreview();
camera.release();
camera = null;
}

private static PreviewCallback previewCallback = new PreviewCallback() {
/**
* {@inheritDoc}
*/
@Override
public void onPreviewFrame(byte[] data, Camera cam) {
if (data == null) throw new NullPointerException();
Camera.Size size = cam.getParameters().getPreviewSize();
if (size == null) throw new NullPointerException();

if (!processing.compareAndSet(false, true)) return;

int width = size.width;
int height = size.height;

int imgAvg = ImageProcessing.decodeYUV420SPtoRedAvg(data.clone(), height, width);
Log.i(TAG, "imgAvg="+imgAvg);
if (imgAvg==0 || imgAvg==255) {
processing.set(false);
return;
}

int averageArrayAvg=0;
int averageArrayCnt=0;
for (int i=0; i<averageArray.length; i++) {
if (averageArray[i]>0) {
averageArrayAvg += averageArray[i];
averageArrayCnt++;
}
}

int rollingAverage = (averageArrayCnt>0)?(averageArrayAvg/averageArrayCnt):0;
TYPE newType = currentType;
if (imgAvg<rollingAverage) {
newType = TYPE.RED;
if (newType!=currentType) {
beats++;
Log.e(TAG, "BEAT!! beats="+beats);
}
} else if (imgAvg>rollingAverage) {
newType = TYPE.GREEN;
}

if (averageIndex==averageArraySize) averageIndex = 0;
averageArray[averageIndex] = imgAvg;
averageIndex++;

//Transitioned from one state to another to the same
if (newType!=currentType) {
currentType=newType;
image.postInvalidate();
}

long endTime = System.currentTimeMillis();
double totalTimeInSecs = (endTime-startTime)/1000d;
if (totalTimeInSecs>=10) {
double bps = (beats/totalTimeInSecs);
int dpm = (int)(bps*60d);
if (dpm<30 || dpm>180) {
startTime = System.currentTimeMillis();
beats = 0;
processing.set(false);
return;
}

Log.e(TAG, "totalTimeInSecs="+totalTimeInSecs+" beats="+beats);

if (beatsIndex==beatsArraySize) beatsIndex = 0;
beatsArray[beatsIndex] = dpm;
beatsIndex++;

int beatsArrayAvg=0;
int beatsArrayCnt=0;
for (int i=0; i<beatsArray.length; i++) {
if (beatsArray[i]>0) {
beatsArrayAvg += beatsArray[i];
beatsArrayCnt++;
}
}
int beatsAvg = (beatsArrayAvg/beatsArrayCnt);
text.setText(String.valueOf(beatsAvg));
startTime = System.currentTimeMillis();
beats = 0;
}
processing.set(false);
}
};

private static SurfaceHolder.Callback surfaceCallback=new SurfaceHolder.Callback() {
/**
* {@inheritDoc}
*/
@Override
public void surfaceCreated(SurfaceHolder holder) {
try {
camera.setPreviewDisplay(previewHolder);
camera.setPreviewCallback(previewCallback);
} catch (Throwable t) {
Log.e("PreviewDemo-surfaceCallback", "Exception in setPreviewDisplay()", t);
}
}

/**
* {@inheritDoc}
*/
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
Camera.Parameters parameters = camera.getParameters();
parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
Camera.Size size = getSmallestPreviewSize(width, height, parameters);
if (size!=null) {
parameters.setPreviewSize(size.width, size.height);
Log.d(TAG, "Using width="+size.width+" height="+size.height);
}
camera.setParameters(parameters);
camera.startPreview();
}

/**
* {@inheritDoc}
*/
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
// Ignore
}
};

private static Camera.Size getSmallestPreviewSize(int width, int height, Camera.Parameters parameters) {
Camera.Size result=null;

for (Camera.Size size : parameters.getSupportedPreviewSizes()) {
if (size.width<=width && size.height<=height) {
if (result==null) {
result=size;
} else {
int resultArea=result.width*result.height;
int newArea=size.width*size.height;

if (newArea<resultArea) result=size;
}
}
}

return result;
}
}

界面部分

   
  package com.jwetherell.heart_rate_monitor;
  

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;

/**
* This class extends the View class and is designed draw the heartbeat image.
*
* @author Justin Wetherell <[email protected]>
*/
public class HeartbeatView extends View {
private static final Matrix matrix = new Matrix();
private static final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

private static Bitmap greenBitmap = null;
private static Bitmap redBitmap = null;

private static int parentWidth = 0;
private static int parentHeight = 0;

public HeartbeatView(Context context, AttributeSet attr) {
super(context,attr);

greenBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.green_icon);
redBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.red_icon);
}

public HeartbeatView(Context context) {
super(context);

greenBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.green_icon);
redBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.red_icon);
}

/**
* {@inheritDoc}
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);

parentWidth = MeasureSpec.getSize(widthMeasureSpec);
parentHeight = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(parentWidth, parentHeight);
}

/**
* {@inheritDoc}
*/
@Override
protected void onDraw(Canvas canvas) {
if (canvas==null) throw new NullPointerException();

Bitmap bitmap = null;
if (HeartRateMonitor.getCurrent()==HeartRateMonitor.TYPE.GREEN) bitmap = greenBitmap;
else bitmap = redBitmap;

int bitmapX = bitmap.getWidth()/2;
int bitmapY = bitmap.getHeight()/2;

int parentX = parentWidth/2;
int parentY = parentHeight/2;

int centerX = parentX-bitmapX;
int centerY = parentY-bitmapY;

matrix.reset();
matrix.postTranslate(centerX, centerY);
canvas.drawBitmap(bitmap, matrix, paint);
}
}

图形处理部分,注意到其处理红颜色的部分,估计是手指表面血液显示的颜色测量的值。

   
  package com.jwetherell.heart_rate_monitor;
  

/**
* This abstract class is used to process images.
*
* @author Justin Wetherell <[email protected]>
*/
public abstract class ImageProcessing {

private static int decodeYUV420SPtoRedSum(byte[] yuv420sp, int width, int height) {
if (yuv420sp==null) return 0;

final int frameSize = width * height;

int sum = 0;
for (int j = 0, yp = 0; j < height; j++) {
int uvp = frameSize + (j >> 1) * width, u = 0, v = 0;
for (int i = 0; i < width; i++, yp++) {
int y = (0xff & ((int) yuv420sp[yp])) - 16;
if (y < 0) y = 0;
if ((i & 1) == 0) {
v = (0xff & yuv420sp[uvp++]) - 128;
u = (0xff & yuv420sp[uvp++]) - 128;
}
int y1192 = 1192 * y;
int r = (y1192 + 1634 * v);
int g = (y1192 - 833 * v - 400 * u);
int b = (y1192 + 2066 * u);

if (r < 0) r = 0; else if (r > 262143) r = 262143;
if (g < 0) g = 0; else if (g > 262143) g = 262143;
if (b < 0) b = 0; else if (b > 262143) b = 262143;

int pixel = 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) & 0xff00) | ((b >> 10) & 0xff);
int red = (pixel >> 16) & 0xff;
sum+=red;
}
}
return sum;
}

/**
* Given a byte array representing a yuv420sp image, determine the average amount of red in the image.
* Note: returns 0 if the byte array is NULL.
*
* @param yuv420sp Byte array representing a yuv420sp image
* @param width Width of the image.
* @param height Height of the image.
* @return int representing the average amount of red in the image.
*/
public static int decodeYUV420SPtoRedAvg(byte[] yuv420sp, int width, int height) {
if (yuv420sp==null) return 0;

final int frameSize = width * height;

int sum = decodeYUV420SPtoRedSum(yuv420sp,width,height);
return (sum/frameSize);
}
}

全部代码请参考 http://code.google.com/p/android-heart-rate-monitor/

600积分一次 answered 11 years, 11 months ago

Your Answer