Android之——模拟实现检测心率变化的应用实例
当今,市面上有了一些可以通过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获取完整模拟实现检测心率变化的应用源代码