Android之——模拟实现检测心率变化的应用实例

转载请注明出处http://blog.csdn.net/l1028386804/article/details/47169025

当今,市面上有了一些可以通过Android应用来检测病人心率,血压,体温,等等,一系列方便人们日常生活的Android手机应用。那么,这些实用的手机应用程序是怎么做出来的呢?那么,今天,我就给大家奉上一个很有意思的应用,那就是Android上模拟实现检测心率的变化。我利用Android模拟实现了通过手机摄像头来感知用户指尖毛细血管的变化来检测心率的功能。哇哦,听起来是不是很高大上呢?瞬间对这个功能充满了膜拜与好奇,有木有?!有木有呢?!哈哈,那就让我们一起来实现这些功能吧。

一、原理

首先我们还是要讲讲这个应用的原理吧,在下认为,要做一个Android应用程序,咱们还要先弄懂它的实现原理吧。不然,看了半天各位都不知道这个应用是基于什么原理做的呢。是吧,那就让我们一起来分析下它的实现原理。

通过摄像头来获得心率,搜了一下这个技术真不是噱头,据说在iPhone早有实现,主要原理是:当打开软件时,手机的闪光灯也会被自动打开,用户将手指放在摄像头上时,指尖皮下血管由于有血液被压入,被光源照射的手指亮度(红色的深度)会有轻微的变化。这个过程可以凭借感光元件捕捉到。这样毛细血管的搏动就能通过画面明度的周期性变化反映出来。

好了,原理说完了,大家有木有看懂呢?

二、实现

1、创建图像处理类ImageProcessing

这个类主要提供处理图像本身的方法。

具体实现如下:

package com.lyz.monitor.utils;

/**
 * 图像处理类
 * @author liuyazhuang
 *
 */
public abstract class ImageProcessing {

	/**
	 * 内部调用的处理图片的方法
	 * @param yuv420sp
	 * @param width
	 * @param height
	 * @return
	 */
	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;
	}

	/**
	 * 对外开放的图像处理方法
	 * @param yuv420sp
	 * @param width
	 * @param height
	 * @return
	 */
	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);
	}
}

2、MainActivity实现

为了简单,我没有单独新建别的类来分解这些功能,我直接在MainActivity中实现了这些功能,那么我们就一起来看看是如何一步步实现的吧。

(1)程序中用到的属性

首先,我们来看看程序中定义了哪些属性字段,来实现这些功能吧。

具体属性字段如下所示:

//曲线
private Timer timer = new Timer();
//Timer任务,与Timer配套使用
private TimerTask task;
private static int gx;
private static int j;

private static double flag=1;
private Handler handler;
private String title = "pulse";
private XYSeries series;
private XYMultipleSeriesDataset mDataset;
private GraphicalView chart;
private XYMultipleSeriesRenderer renderer;
private Context context;
private int addX = -1;
double addY;
int[] xv = new int[300];
int[] yv = new int[300];
int[] hua=new int[]{9,10,11,12,13,14,13,12,11,10,9,8,7,6,7,8,9,10,11,10,10};

//	private static final String TAG = "HeartRateMonitor";
private static final AtomicBoolean processing = new AtomicBoolean(false);
//Android手机预览控件
private static SurfaceView preview = null;
//预览设置信息
private static SurfaceHolder previewHolder = null;
//Android手机相机句柄
private static Camera camera = null;
//private static View image = null;
private static TextView text = null;
private static TextView text1 = null;
private static TextView text2 = 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];
//设置默认类型
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;

咋一看,是不是很多?有木有一种头晕乎乎的赶脚呢?没关系,通过后面具体的功能实现,相信大家能弄明白每个属性字段的作用与含义的。不要掉队,继续认真向下看哦。

(2)定义枚举类型来标识当前颜色

颜色类型,我在这里用一个枚举类型来定义,这个枚举类型很简单,只有两种颜色,一种是绿色,一种是红色。默认颜色为绿色。

具体实现的代码如下:

/**
	 * 类型枚举
	 * @author liuyazhuang
	 *
	 */
	public static enum TYPE {
		GREEN, RED
	};
	//设置默认类型
	private static TYPE currentType = TYPE.GREEN;
	//获取当前类型
	public static TYPE getCurrent() {
		return currentType;
	}
(3)初始化配置方法initConfig

这个方法总体上的功能是初始化程序的各个配置,包括调用其他方法,例如页面图表的初始化,UI控件的初始化,应用程序启动后显示的样式,调用相机,通过Handler接收其他方法传递过来的消息信息来更新UI,,等等。主要是实现应用的配置功能,同时这个方法相当于一个应用程序的管家,它来直接或间接的调用其他方法,来使整个应用程序顺利运行起来。

具体代码实现如下:

/**
	 * 初始化配置
	 */
	private void initConfig() {
		//曲线
		context = getApplicationContext();

		//这里获得main界面上的布局,下面会把图表画在这个布局里面
		LinearLayout layout = (LinearLayout)findViewById(R.id.linearLayout1);

		//这个类用来放置曲线上的所有点,是一个点的集合,根据这些点画出曲线
		series = new XYSeries(title);

		//创建一个数据集的实例,这个数据集将被用来创建图表
		mDataset = new XYMultipleSeriesDataset();

		//将点集添加到这个数据集中
		mDataset.addSeries(series);

		//以下都是曲线的样式和属性等等的设置,renderer相当于一个用来给图表做渲染的句柄
		int color = Color.GREEN;
		PointStyle style = PointStyle.CIRCLE;
		renderer = buildRenderer(color, style, true);

		//设置好图表的样式
		setChartSettings(renderer, "X", "Y", 0, 300, 4, 16, Color.WHITE, Color.WHITE);

		//生成图表
		chart = ChartFactory.getLineChartView(context, mDataset, renderer);

		//将图表添加到布局中去
		layout.addView(chart, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));

		//这里的Handler实例将配合下面的Timer实例,完成定时更新图表的功能
		handler = new Handler() {
			@Override
			public void handleMessage(Message msg) {
				//        		刷新图表
				updateChart();
				super.handleMessage(msg);
			}
		};

		task = new TimerTask() {
			@Override
			public void run() {
				Message message = new Message();
				message.what = 1;
				handler.sendMessage(message);
			}
		};

		timer.schedule(task, 1,20);           //曲线
		//获取SurfaceView控件
		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);
		text1 = (TextView) findViewById(R.id.text1);
		text2 = (TextView) findViewById(R.id.text2);
		PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
		wakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, "DoNotDimScreen");
	}
(4)创建图表的方法buildRenderer

这个方法主要是利用了第三方的类库类实现创建图标的操作。

具体代码实现如下

/**
	 * 创建图表
	 * @param color
	 * @param style
	 * @param fill
	 * @return
	 */
	protected XYMultipleSeriesRenderer buildRenderer(int color, PointStyle style, boolean fill) {
		XYMultipleSeriesRenderer renderer = new XYMultipleSeriesRenderer();

		//设置图表中曲线本身的样式,包括颜色、点的大小以及线的粗细等
		XYSeriesRenderer r = new XYSeriesRenderer();
		r.setColor(Color.RED);
//		r.setPointStyle(null);
//		r.setFillPoints(fill);
		r.setLineWidth(1);
		renderer.addSeriesRenderer(r);
		return renderer;
	}
(5)设置图表的样式方法setChartSettings

这个方法主要是对(4)中创建的图表,进行样式的设置。

具体代码如下:

/**
	 * 设置图标的样式
	 * @param renderer
	 * @param xTitle:x标题
	 * @param yTitle:y标题
	 * @param xMin:x最小长度
	 * @param xMax:x最大长度
	 * @param yMin:y最小长度
	 * @param yMax:y最大长度
	 * @param axesColor:颜色
	 * @param labelsColor:标签
	 */
	protected void setChartSettings(XYMultipleSeriesRenderer renderer, String xTitle, String yTitle,
			double xMin, double xMax, double yMin, double yMax, int axesColor, int labelsColor) {
		//有关对图表的渲染可参看api文档
		renderer.setChartTitle(title);
		renderer.setXTitle(xTitle);
		renderer.setYTitle(yTitle);
		renderer.setXAxisMin(xMin);
		renderer.setXAxisMax(xMax);
		renderer.setYAxisMin(yMin);
		renderer.setYAxisMax(yMax);
		renderer.setAxesColor(axesColor);
		renderer.setLabelsColor(labelsColor);
		renderer.setShowGrid(true);
		renderer.setGridColor(Color.GREEN);
		renderer.setXLabels(20);
		renderer.setYLabels(10);
		renderer.setXTitle("Time");
		renderer.setYTitle("mmHg");
		renderer.setYLabelsAlign(Align.RIGHT);
		renderer.setPointSize((float) 3 );
		renderer.setShowLegend(false);
	}
(6)更新图表updateChart

这个方法主要实现了对图表中曲线图的更新绘制,同时检测手机摄像头感应的手指位置,如果手指位置不正确,则会提示“请用您的指尖盖住摄像头镜头”的信息来提示用户。动态的更新绘制曲线图来模拟用户心跳频率。

具体代码实现如下:

/**
	 * 更新图标信息
	 */
	private void updateChart() {

		//设置好下一个需要增加的节点
		if(flag==1)
			addY=10;
		else{
//			addY=250;
			flag=1;
			if(gx<200){
				if(hua[20]>1){
					Toast.makeText(MainActivity.this, "请用您的指尖盖住摄像头镜头!", Toast.LENGTH_SHORT).show();
					hua[20]=0;}
				hua[20]++;
				return;}
			else
				hua[20]=10;
			j=0;

		}
		if(j<20){
			addY=hua[j];
			j++;
		}

		//移除数据集中旧的点集
		mDataset.removeSeries(series);

		//判断当前点集中到底有多少点,因为屏幕总共只能容纳100个,所以当点数超过100时,长度永远是100
		int length = series.getItemCount();
		int bz=0;
		//addX = length;
		if (length > 300) {
			length = 300;
			bz=1;
		}
		addX = length;
		//将旧的点集中x和y的数值取出来放入backup中,并且将x的值加1,造成曲线向右平移的效果
		for (int i = 0; i < length; i++) {
			xv[i] = (int) series.getX(i) -bz;
			yv[i] = (int) series.getY(i);
		}

		//点集先清空,为了做成新的点集而准备
		series.clear();
		mDataset.addSeries(series);
		//将新产生的点首先加入到点集中,然后在循环体中将坐标变换后的一系列点都重新加入到点集中
		//这里可以试验一下把顺序颠倒过来是什么效果,即先运行循环体,再添加新产生的点
		series.add(addX, addY);
		for (int k = 0; k < length; k++) {
			series.add(xv[k], yv[k]);
		}
		//在数据集中添加新的点集
		//mDataset.addSeries(series);

		//视图更新,没有这一步,曲线不会呈现动态
		//如果在非UI主线程中,需要调用postInvalidate(),具体参考api
		chart.invalidate();
	} //曲线
(7)相机预览回调方法previewCallback

这个方法中实现动态更新界面UI的功能,通过获取手机摄像头的参数来实时动态计算平均像素值、脉冲数,从而实时动态计算心率值。

具体代码实现如下:

/**
	 * 相机预览方法
	 * 这个方法中实现动态更新界面UI的功能,
	 * 通过获取手机摄像头的参数来实时动态计算平均像素值、脉冲数,从而实时动态计算心率值。
	 */
	private static PreviewCallback previewCallback = new PreviewCallback() {
		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);
			gx=imgAvg;
			text1.setText("平均像素值是"+String.valueOf(imgAvg));
			//像素平均值imgAvg,日志
			//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++;
					flag=0;
					text2.setText("脉冲数是"+String.valueOf(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();
			}
			//获取系统结束时间(ms)
			long endTime = System.currentTimeMillis();
			double totalTimeInSecs = (endTime - startTime) / 1000d;
			if (totalTimeInSecs >= 2) {
				double bps = (beats / totalTimeInSecs);
				int dpm = (int) (bps * 60d);
				if (dpm < 30 || dpm > 180||imgAvg<200) {
					//获取系统开始时间(ms)
					startTime = System.currentTimeMillis();
					//beats心跳总数
					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)+"  zhi:"+String.valueOf(beatsArray.length)
						+"    "+String.valueOf(beatsIndex)+"    "+String.valueOf(beatsArrayAvg)+"    "+String.valueOf(beatsArrayCnt));
				//获取系统时间(ms)
				startTime = System.currentTimeMillis();
				beats = 0;
			}
			processing.set(false);
		}
	};
(8)SurfaceHolder.Callback

这个方法主要是相机摄像头,捕捉信息改变时调用。

具体代码实现如下:

/**
	 * 预览回调接口
	 */
	private static SurfaceHolder.Callback surfaceCallback = new SurfaceHolder.Callback() {
		//创建时调用
		@Override
		public void surfaceCreated(SurfaceHolder holder) {
			try {
				camera.setPreviewDisplay(previewHolder);
				camera.setPreviewCallback(previewCallback);
			} catch (Throwable t) {
				Log.e("PreviewDemo-surfaceCallback","Exception in setPreviewDisplay()", t);
			}
		}
		//当预览改变的时候回调此方法
		@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();
		}
		//销毁的时候调用
		@Override
		public void surfaceDestroyed(SurfaceHolder holder) {
			// Ignore
		}
	};
(9)获取相机最小的预览尺寸方法getSmallestPreviewSize

这个方法的功能是获取当前手机相机最小的预览尺寸。

具体代码实现如下:

/**
	 * 获取相机最小的预览尺寸
	 * @param width
	 * @param height
	 * @param parameters
	 * @return
	 */
	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;
	}
(10)onCreate方法

这个方法是Android原生自带的方法,通常在这个方法中我们会实现页面控件的初始化以及一些数据的初始化工作。我们这个项目中,主要是设置要显示的UI和调用initConfig方法来启动应用程序的配置,从而实现应用程序的顺利运行。

具体实现代码如下:

@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		initConfig();
	}
(11)其他一些Android原生自带方法
@Override
public void onDestroy() {
	//当结束程序时关掉Timer
	timer.cancel();
	super.onDestroy();
};
@Override
	public void onConfigurationChanged(Configuration newConfig) {
	super.onConfigurationChanged(newConfig);
}

@Override
public void onResume() {
	super.onResume();
	wakeLock.acquire();
	camera = Camera.open();
	startTime = System.currentTimeMillis();
}

@Override
public void onPause() {
	super.onPause();
	wakeLock.release();
	camera.setPreviewCallback(null);
	camera.stopPreview();
	camera.release();
	camera = null;
}
(12)MainActivity完整代码

最后我还是给出MainActivity的完整代码吧,大家根据上面的分析仔细阅读几遍,便会体会这其中的奥妙了。嘿嘿,加油哦!

package com.lyz.xinlv.activity;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicBoolean;

import org.achartengine.ChartFactory;
import org.achartengine.GraphicalView;
import org.achartengine.chart.PointStyle;
import org.achartengine.model.XYMultipleSeriesDataset;
import org.achartengine.model.XYSeries;
import org.achartengine.renderer.XYMultipleSeriesRenderer;
import org.achartengine.renderer.XYSeriesRenderer;

import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Color;
import android.graphics.Paint.Align;
import android.hardware.Camera;
import android.hardware.Camera.PreviewCallback;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.ViewGroup.LayoutParams;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;

import com.lyz.monitor.utils.ImageProcessing;

/**
 * 程序的主入口
 * @author liuyazhuang
 *
 */
public class MainActivity extends Activity {
	//曲线
	private Timer timer = new Timer();
	//Timer任务,与Timer配套使用
	private TimerTask task;
	private static int gx;
	private static int j;

	private static double flag=1;
	private Handler handler;
	private String title = "pulse";
	private XYSeries series;
	private XYMultipleSeriesDataset mDataset;
	private GraphicalView chart;
	private XYMultipleSeriesRenderer renderer;
	private Context context;
	private int addX = -1;
	double addY;
	int[] xv = new int[300];
	int[] yv = new int[300];
	int[] hua=new int[]{9,10,11,12,13,14,13,12,11,10,9,8,7,6,7,8,9,10,11,10,10};

	//	private static final String TAG = "HeartRateMonitor";
	private static final AtomicBoolean processing = new AtomicBoolean(false);
	//Android手机预览控件
	private static SurfaceView preview = null;
	//预览设置信息
	private static SurfaceHolder previewHolder = null;
	//Android手机相机句柄
	private static Camera camera = null;
	//private static View image = null;
	private static TextView text = null;
	private static TextView text1 = null;
	private static TextView text2 = 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];

	/**
	 * 类型枚举
	 * @author liuyazhuang
	 *
	 */
	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;

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		initConfig();
	}

	/**
	 * 初始化配置
	 */
	private void initConfig() {
		//曲线
		context = getApplicationContext();

		//这里获得main界面上的布局,下面会把图表画在这个布局里面
		LinearLayout layout = (LinearLayout)findViewById(R.id.linearLayout1);

		//这个类用来放置曲线上的所有点,是一个点的集合,根据这些点画出曲线
		series = new XYSeries(title);

		//创建一个数据集的实例,这个数据集将被用来创建图表
		mDataset = new XYMultipleSeriesDataset();

		//将点集添加到这个数据集中
		mDataset.addSeries(series);

		//以下都是曲线的样式和属性等等的设置,renderer相当于一个用来给图表做渲染的句柄
		int color = Color.GREEN;
		PointStyle style = PointStyle.CIRCLE;
		renderer = buildRenderer(color, style, true);

		//设置好图表的样式
		setChartSettings(renderer, "X", "Y", 0, 300, 4, 16, Color.WHITE, Color.WHITE);

		//生成图表
		chart = ChartFactory.getLineChartView(context, mDataset, renderer);

		//将图表添加到布局中去
		layout.addView(chart, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));

		//这里的Handler实例将配合下面的Timer实例,完成定时更新图表的功能
		handler = new Handler() {
			@Override
			public void handleMessage(Message msg) {
				//        		刷新图表
				updateChart();
				super.handleMessage(msg);
			}
		};

		task = new TimerTask() {
			@Override
			public void run() {
				Message message = new Message();
				message.what = 1;
				handler.sendMessage(message);
			}
		};

		timer.schedule(task, 1,20);           //曲线
		//获取SurfaceView控件
		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);
		text1 = (TextView) findViewById(R.id.text1);
		text2 = (TextView) findViewById(R.id.text2);
		PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
		wakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, "DoNotDimScreen");
	}

	//	曲线
	@Override
	public void onDestroy() {
		//当结束程序时关掉Timer
		timer.cancel();
		super.onDestroy();
	};

	/**
	 * 创建图表
	 * @param color
	 * @param style
	 * @param fill
	 * @return
	 */
	protected XYMultipleSeriesRenderer buildRenderer(int color, PointStyle style, boolean fill) {
		XYMultipleSeriesRenderer renderer = new XYMultipleSeriesRenderer();

		//设置图表中曲线本身的样式,包括颜色、点的大小以及线的粗细等
		XYSeriesRenderer r = new XYSeriesRenderer();
		r.setColor(Color.RED);
//		r.setPointStyle(null);
//		r.setFillPoints(fill);
		r.setLineWidth(1);
		renderer.addSeriesRenderer(r);
		return renderer;
	}

	/**
	 * 设置图标的样式
	 * @param renderer
	 * @param xTitle:x标题
	 * @param yTitle:y标题
	 * @param xMin:x最小长度
	 * @param xMax:x最大长度
	 * @param yMin:y最小长度
	 * @param yMax:y最大长度
	 * @param axesColor:颜色
	 * @param labelsColor:标签
	 */
	protected void setChartSettings(XYMultipleSeriesRenderer renderer, String xTitle, String yTitle,
			double xMin, double xMax, double yMin, double yMax, int axesColor, int labelsColor) {
		//有关对图表的渲染可参看api文档
		renderer.setChartTitle(title);
		renderer.setXTitle(xTitle);
		renderer.setYTitle(yTitle);
		renderer.setXAxisMin(xMin);
		renderer.setXAxisMax(xMax);
		renderer.setYAxisMin(yMin);
		renderer.setYAxisMax(yMax);
		renderer.setAxesColor(axesColor);
		renderer.setLabelsColor(labelsColor);
		renderer.setShowGrid(true);
		renderer.setGridColor(Color.GREEN);
		renderer.setXLabels(20);
		renderer.setYLabels(10);
		renderer.setXTitle("Time");
		renderer.setYTitle("mmHg");
		renderer.setYLabelsAlign(Align.RIGHT);
		renderer.setPointSize((float) 3 );
		renderer.setShowLegend(false);
	}

	/**
	 * 更新图标信息
	 */
	private void updateChart() {

		//设置好下一个需要增加的节点
		if(flag==1)
			addY=10;
		else{
//			addY=250;
			flag=1;
			if(gx<200){
				if(hua[20]>1){
					Toast.makeText(MainActivity.this, "请用您的指尖盖住摄像头镜头!", Toast.LENGTH_SHORT).show();
					hua[20]=0;}
				hua[20]++;
				return;}
			else
				hua[20]=10;
			j=0;

		}
		if(j<20){
			addY=hua[j];
			j++;
		}

		//移除数据集中旧的点集
		mDataset.removeSeries(series);

		//判断当前点集中到底有多少点,因为屏幕总共只能容纳100个,所以当点数超过100时,长度永远是100
		int length = series.getItemCount();
		int bz=0;
		//addX = length;
		if (length > 300) {
			length = 300;
			bz=1;
		}
		addX = length;
		//将旧的点集中x和y的数值取出来放入backup中,并且将x的值加1,造成曲线向右平移的效果
		for (int i = 0; i < length; i++) {
			xv[i] = (int) series.getX(i) -bz;
			yv[i] = (int) series.getY(i);
		}

		//点集先清空,为了做成新的点集而准备
		series.clear();
		mDataset.addSeries(series);
		//将新产生的点首先加入到点集中,然后在循环体中将坐标变换后的一系列点都重新加入到点集中
		//这里可以试验一下把顺序颠倒过来是什么效果,即先运行循环体,再添加新产生的点
		series.add(addX, addY);
		for (int k = 0; k < length; k++) {
			series.add(xv[k], yv[k]);
		}
		//在数据集中添加新的点集
		//mDataset.addSeries(series);

		//视图更新,没有这一步,曲线不会呈现动态
		//如果在非UI主线程中,需要调用postInvalidate(),具体参考api
		chart.invalidate();
	} //曲线

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

	@Override
	public void onResume() {
		super.onResume();
		wakeLock.acquire();
		camera = Camera.open();
		startTime = System.currentTimeMillis();
	}

	@Override
	public void onPause() {
		super.onPause();
		wakeLock.release();
		camera.setPreviewCallback(null);
		camera.stopPreview();
		camera.release();
		camera = null;
	}

	/**
	 * 相机预览方法
	 * 这个方法中实现动态更新界面UI的功能,
	 * 通过获取手机摄像头的参数来实时动态计算平均像素值、脉冲数,从而实时动态计算心率值。
	 */
	private static PreviewCallback previewCallback = new PreviewCallback() {
		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);
			gx=imgAvg;
			text1.setText("平均像素值是"+String.valueOf(imgAvg));
			//像素平均值imgAvg,日志
			//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++;
					flag=0;
					text2.setText("脉冲数是"+String.valueOf(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();
			}
			//获取系统结束时间(ms)
			long endTime = System.currentTimeMillis();
			double totalTimeInSecs = (endTime - startTime) / 1000d;
			if (totalTimeInSecs >= 2) {
				double bps = (beats / totalTimeInSecs);
				int dpm = (int) (bps * 60d);
				if (dpm < 30 || dpm > 180||imgAvg<200) {
					//获取系统开始时间(ms)
					startTime = System.currentTimeMillis();
					//beats心跳总数
					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)+"  zhi:"+String.valueOf(beatsArray.length)
						+"    "+String.valueOf(beatsIndex)+"    "+String.valueOf(beatsArrayAvg)+"    "+String.valueOf(beatsArrayCnt));
				//获取系统时间(ms)
				startTime = System.currentTimeMillis();
				beats = 0;
			}
			processing.set(false);
		}
	};

	/**
	 * 预览回调接口
	 */
	private static SurfaceHolder.Callback surfaceCallback = new SurfaceHolder.Callback() {
		//创建时调用
		@Override
		public void surfaceCreated(SurfaceHolder holder) {
			try {
				camera.setPreviewDisplay(previewHolder);
				camera.setPreviewCallback(previewCallback);
			} catch (Throwable t) {
				Log.e("PreviewDemo-surfaceCallback","Exception in setPreviewDisplay()", t);
			}
		}
		//当预览改变的时候回调此方法
		@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();
		}
		//销毁的时候调用
		@Override
		public void surfaceDestroyed(SurfaceHolder holder) {
			// Ignore
		}
	};

	/**
	 * 获取相机最小的预览尺寸
	 * @param width
	 * @param height
	 * @param parameters
	 * @return
	 */
	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;
	}
}

3、UI布局

这里就不多说了,对于UI布局的实现,相信大家看了源码都懂得。

具体代码实现如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <SurfaceView
        android:id="@+id/preview"
        android:layout_width="fill_parent"
        android:layout_height="200dp"
        android:layout_marginLeft="50dip"
        android:layout_marginRight="50dip" />

    <LinearLayout
        android:id="@+id/linearLayout1"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:orientation="vertical" >
    </LinearLayout>

    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="50dip"
        android:layout_weight="1"
        android:text="@string/show" >
    </TextView>

    <TextView
        android:id="@+id/text1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="50dip"
        android:layout_weight="1"
        android:text="@string/show" >
    </TextView>

    <TextView
        android:id="@+id/text2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="50dip"
        android:layout_weight="1"
        android:text="@string/show" >
    </TextView>

</LinearLayout>

4、strings.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <string name="app_name">心率检测</string>
    <string name="action_settings">Settings</string>
    <string name="hello_world">Hello world!</string>
	<string name="show">显示</string>
</resources>

5、授权AndroidManifest.xml

最后,我把授权文件贴出来吧。

具体代码实现如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.lyz.xinlv.activity"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="15"
        android:targetSdkVersion="15" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-feature android:name="android.hardware.camera" />
    <uses-feature android:name="android.hardware.camera.autofocus" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.lyz.xinlv.activity.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

至此,这个应用开发完成,亲们,是不是比想象的简单呢?

三、运行效果

四、温馨提示

本实例中,为了方面,我把一些文字直接写在了布局文件中和相关的类中,大家在真实的项目中要把这些文字写在string.xml文件中,在外部引用这些资源,切记,这是作为一个Android程序员最基本的开发常识和规范,我在这里只是为了方便直接写在了类和布局文件中。

大家可以到链接http://download.csdn.net/detail/l1028386804/8949597获取完整模拟实现检测心率变化的应用源代码

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-11-02 05:33:00

Android之——模拟实现检测心率变化的应用实例的相关文章

处理Android程序运行时的配置变化

本篇文章翻译自Android官方文档Handling Runtime Changes,有翻译错误请留言告知,多谢. Android程序在运行期间设备的配置是可能发生改变的(例如屏幕的方向,键盘可用性,和语言等).当这些配置发生变化时,Android会重启正在运行的Activity(先调用onDestory(),紧接着调用onCreate()).这个设计是为了让你的程序在配置发生变化时,使用不同的资源自动去适配新的配置机器. 正确的处理重启,一件很重要的事就是通过Activity正常的生命周期去恢

检测网络变化(wifi、2g、3g、4g)

检测网络变化(wifi.2g.3g.4g) 1.注册广播"android.net.conn.CONNECTIVITY_CHANGE"和"android.net.wifi.STATE_CHANGE" 进行监听: private class ConnectivityBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent in

心跳之旅—&#128151;—iOS用手机摄像头检测心率(PPG)

[前情提要] 光阴似箭,日月如梭,最近几年,支持心率检测的设备愈发常见了,大家都在各种测空气测雪碧的,如火如荼,于是我也来凑一凑热闹.[0]这段时间,我完成了一个基于iOS的心率检测Demo,只要稳定地用指尖按住手机摄像头,它就能采集你的心率数据.Demo完成后,我对心率检测组件进行了封装,并提供了默认动画和音效,能够非常方便导入到其他项目中.在这篇博客里,我将向大家分享一下我完成心率检测的过程,以及,期间我遇到的种种困难. 本文中涉及到的要点主要有: AVCapture Core Graphi

Android Studio代码自动检测错误提示

Android Studio的代码自动检测的错误提示方式感觉有点奇葩,和Eclipse差别很大,Eclipse检测到某个资源文件找不到或者错误,都会在Project中对应的文件前面打叉,但是Android Studio不用这种方式,所以估计你刚开始找半天找不到错误提示到底在哪?这个错误提示的方式是直接进行了整理归类,不像Eclipse在对应的文件前打叉显示.那如何打开错误提示的列表,如下图: 做个补充,可能很多人会找不到Message在哪,其实你只要选择Build,然后把工程clean或者reb

android 监听网络状态的变化及实战

android 监听网络状态的变化及实际应用 转载请注明博客地址:http://blog.csdn.net/gdutxiaoxu/article/details/53008266 平时我们在请求错误的情况下,通常会进行处理一下,一般来说,主要分为两方面的错误 - 没有网络的错误 - 在有网络的情况下,我们客户端的错误或者服务器端的错误 今天这篇博客主要阐述以下问题 怎样监听网络状态的变化,包括是否打开WiFi,否打开数据网络,当前连接的网络是否可用 网络没有打开情况下的处理,如弹出对话框,跳转到

【android】模拟点击某个指定坐标作用在View上

/** * 模拟点击某个指定坐标作用在View上 * @param view * @param x * @param y */ public void clickView(View view,float x,float y) { long downTime = SystemClock.uptimeMillis(); final MotionEvent downEvent = MotionEvent.obtain( downTime, downTime, MotionEvent.ACTION_DO

android的apk自动检测升级

首先获取本地apk版本: /** * 获取本地软件版本 */ public static int getLocalVersion(Context ctx){ int localVersion = 0; try { PackageInfo packageInfo = ctx.getApplicationContext() .getPackageManager().getPackageInfo(ctx.getPackageName(), 0); localVersion = packageInfo.

android 实现模拟按键

android 实现模拟按键方法一 通过Runtime实现,代码如下: try { String keyCommand = "input keyevent " + KeyEvent.KEYCODE_MENU; Runtime runtime = Runtime.getRuntime(); Process proc = runtime.exec(keyCommand); } catch (IOException e) { // TODO Auto-generated catch bloc

一步一步图示开发第一个Android项目并运行 看图学Android---Android 开发实例教程二

一.生成一个Android应用工程 1.运行eclipse.exe 2.见下图 3.进入界面 4.输入 应用.项目.包名称 5.选择 项目建立的工作空间路径: 6.配置 图标和背景等特性 7.选择 Activity  的页面格式 8.设置对应名称 9.生成项目 二. 项目介绍 1.界面 可以使用工具采用拖拉的方式 拖动控件.  图形界面开发环境. 2.界面布局的代码格式 3.MainActivity.java  主程序结构   代码和视图绑定 3.代码MainActivity.java pack