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

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

一、原理

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

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

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

二、实现

1、创建图像处理类ImageProcessing

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

具体实现如下:

  1. package com.lyz.monitor.utils;
  2. /**
  3. * 图像处理类
  4. * @author liuyazhuang
  5. *
  6. */
  7. public abstract class ImageProcessing {
  8. /**
  9. * 内部调用的处理图片的方法
  10. * @param yuv420sp
  11. * @param width
  12. * @param height
  13. * @return
  14. */
  15. private static int decodeYUV420SPtoRedSum(byte[] yuv420sp, int width,int height) {
  16. if (yuv420sp == null)
  17. return 0;
  18. final int frameSize = width * height;
  19. int sum = 0;
  20. for (int j = 0, yp = 0; j < height; j ) {
  21. int uvp = frameSize (j >> 1) * width, u = 0, v = 0;
  22. for (int i = 0; i < width; i , yp ) {
  23. int y = (0xff & ((int) yuv420sp[yp])) - 16;
  24. if (y < 0)
  25. y = 0;
  26. if ((i & 1) == 0) {
  27. v = (0xff & yuv420sp[uvp ]) - 128;
  28. u = (0xff & yuv420sp[uvp ]) - 128;
  29. }
  30. int y1192 = 1192 * y;
  31. int r = (y1192 1634 * v);
  32. int g = (y1192 - 833 * v - 400 * u);
  33. int b = (y1192 2066 * u);
  34. if (r < 0)
  35. r = 0;
  36. else if (r > 262143)
  37. r = 262143;
  38. if (g < 0)
  39. g = 0;
  40. else if (g > 262143)
  41. g = 262143;
  42. if (b < 0)
  43. b = 0;
  44. else if (b > 262143)
  45. b = 262143;
  46. int pixel = 0xff000000 | ((r << 6) & 0xff0000)
  47. | ((g >> 2) & 0xff00) | ((b >> 10) & 0xff);
  48. int red = (pixel >> 16) & 0xff;
  49. sum = red;
  50. }
  51. }
  52. return sum;
  53. }
  54. /**
  55. * 对外开放的图像处理方法
  56. * @param yuv420sp
  57. * @param width
  58. * @param height
  59. * @return
  60. */
  61. public static int decodeYUV420SPtoRedAvg(byte[] yuv420sp, int width,
  62. int height) {
  63. if (yuv420sp == null)
  64. return 0;
  65. final int frameSize = width * height;
  66. int sum = decodeYUV420SPtoRedSum(yuv420sp, width, height);
  67. return (sum / frameSize);
  68. }
  69. }

2、MainActivity实现

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

(1)程序中用到的属性

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

具体属性字段如下所示:

  1. //曲线
  2. private Timer timer = new Timer();
  3. //Timer任务,与Timer配套使用
  4. private TimerTask task;
  5. private static int gx;
  6. private static int j;
  7. private static double flag=1;
  8. private Handler handler;
  9. private String title = "pulse";
  10. private XYSeries series;
  11. private XYMultipleSeriesDataset mDataset;
  12. private GraphicalView chart;
  13. private XYMultipleSeriesRenderer renderer;
  14. private Context context;
  15. private int addX = -1;
  16. double addY;
  17. int[] xv = new int[300];
  18. int[] yv = new int[300];
  19. 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};
  20. // private static final String TAG = "HeartRateMonitor";
  21. private static final AtomicBoolean processing = new AtomicBoolean(false);
  22. //Android手机预览控件
  23. private static SurfaceView preview = null;
  24. //预览设置信息
  25. private static SurfaceHolder previewHolder = null;
  26. //Android手机相机句柄
  27. private static Camera camera = null;
  28. //private static View image = null;
  29. private static TextView text = null;
  30. private static TextView text1 = null;
  31. private static TextView text2 = null;
  32. private static WakeLock wakeLock = null;
  33. private static int averageIndex = 0;
  34. private static final int averageArraySize = 4;
  35. private static final int[] averageArray = new int[averageArraySize];
  36. //设置默认类型
  37. private static TYPE currentType = TYPE.GREEN;
  38. //获取当前类型
  39. public static TYPE getCurrent() {
  40. return currentType;
  41. }
  42. //心跳下标值
  43. private static int beatsIndex = 0;
  44. //心跳数组的大小
  45. private static final int beatsArraySize = 3;
  46. //心跳数组
  47. private static final int[] beatsArray = new int[beatsArraySize];
  48. //心跳脉冲
  49. private static double beats = 0;
  50. //开始时间
  51. private static long startTime = 0;

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

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

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

具体实现的代码如下:

  1. /**
  2. * 类型枚举
  3. * @author liuyazhuang
  4. *
  5. */
  6. public static enum TYPE {
  7. GREEN, RED
  8. };
  9. //设置默认类型
  10. private static TYPE currentType = TYPE.GREEN;
  11. //获取当前类型
  12. public static TYPE getCurrent() {
  13. return currentType;
  14. }
(3)初始化配置方法initConfig

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

具体代码实现如下:

  1. /**
  2. * 初始化配置
  3. */
  4. private void initConfig() {
  5. //曲线
  6. context = getApplicationContext();
  7. //这里获得main界面上的布局,下面会把图表画在这个布局里面
  8. LinearLayout layout = (LinearLayout)findViewById(R.id.linearLayout1);
  9. //这个类用来放置曲线上的所有点,是一个点的集合,根据这些点画出曲线
  10. series = new XYSeries(title);
  11. //创建一个数据集的实例,这个数据集将被用来创建图表
  12. mDataset = new XYMultipleSeriesDataset();
  13. //将点集添加到这个数据集中
  14. mDataset.addSeries(series);
  15. //以下都是曲线的样式和属性等等的设置,renderer相当于一个用来给图表做渲染的句柄
  16. int color = Color.GREEN;
  17. PointStyle style = PointStyle.CIRCLE;
  18. renderer = buildRenderer(color, style, true);
  19. //设置好图表的样式
  20. setChartSettings(renderer, "X", "Y", 0, 300, 4, 16, Color.WHITE, Color.WHITE);
  21. //生成图表
  22. chart = ChartFactory.getLineChartView(context, mDataset, renderer);
  23. //将图表添加到布局中去
  24. layout.addView(chart, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
  25. //这里的Handler实例将配合下面的Timer实例,完成定时更新图表的功能
  26. handler = new Handler() {
  27. @Override
  28. public void handleMessage(Message msg) {
  29. // 刷新图表
  30. updateChart();
  31. super.handleMessage(msg);
  32. }
  33. };
  34. task = new TimerTask() {
  35. @Override
  36. public void run() {
  37. Message message = new Message();
  38. message.what = 1;
  39. handler.sendMessage(message);
  40. }
  41. };
  42. timer.schedule(task, 1,20); //曲线
  43. //获取SurfaceView控件
  44. preview = (SurfaceView) findViewById(R.id.preview);
  45. previewHolder = preview.getHolder();
  46. previewHolder.addCallback(surfaceCallback);
  47. previewHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
  48. // image = findViewById(R.id.image);
  49. text = (TextView) findViewById(R.id.text);
  50. text1 = (TextView) findViewById(R.id.text1);
  51. text2 = (TextView) findViewById(R.id.text2);
  52. PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
  53. wakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, "DoNotDimScreen");
  54. }
(4)创建图表的方法buildRenderer

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

具体代码实现如下

  1. /**
  2. * 创建图表
  3. * @param color
  4. * @param style
  5. * @param fill
  6. * @return
  7. */
  8. protected XYMultipleSeriesRenderer buildRenderer(int color, PointStyle style, boolean fill) {
  9. XYMultipleSeriesRenderer renderer = new XYMultipleSeriesRenderer();
  10. //设置图表中曲线本身的样式,包括颜色、点的大小以及线的粗细等
  11. XYSeriesRenderer r = new XYSeriesRenderer();
  12. r.setColor(Color.RED);
  13. // r.setPointStyle(null);
  14. // r.setFillPoints(fill);
  15. r.setLineWidth(1);
  16. renderer.addSeriesRenderer(r);
  17. return renderer;
  18. }
(5)设置图表的样式方法setChartSettings

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

具体代码如下:

  1. /**
  2. * 设置图标的样式
  3. * @param renderer
  4. * @param xTitle:x标题
  5. * @param yTitle:y标题
  6. * @param xMin:x最小长度
  7. * @param xMax:x最大长度
  8. * @param yMin:y最小长度
  9. * @param yMax:y最大长度
  10. * @param axesColor:颜色
  11. * @param labelsColor:标签
  12. */
  13. protected void setChartSettings(XYMultipleSeriesRenderer renderer, String xTitle, String yTitle,
  14. double xMin, double xMax, double yMin, double yMax, int axesColor, int labelsColor) {
  15. //有关对图表的渲染可参看api文档
  16. renderer.setChartTitle(title);
  17. renderer.setXTitle(xTitle);
  18. renderer.setYTitle(yTitle);
  19. renderer.setXAxisMin(xMin);
  20. renderer.setXAxisMax(xMax);
  21. renderer.setYAxisMin(yMin);
  22. renderer.setYAxisMax(yMax);
  23. renderer.setAxesColor(axesColor);
  24. renderer.setLabelsColor(labelsColor);
  25. renderer.setShowGrid(true);
  26. renderer.setGridColor(Color.GREEN);
  27. renderer.setXLabels(20);
  28. renderer.setYLabels(10);
  29. renderer.setXTitle("Time");
  30. renderer.setYTitle("mmHg");
  31. renderer.setYLabelsAlign(Align.RIGHT);
  32. renderer.setPointSize((float) 3 );
  33. renderer.setShowLegend(false);
  34. }
(6)更新图表updateChart

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

具体代码实现如下:

  1. /**
  2. * 更新图标信息
  3. */
  4. private void updateChart() {
  5. //设置好下一个需要增加的节点
  6. if(flag==1)
  7. addY=10;
  8. else{
  9. // addY=250;
  10. flag=1;
  11. if(gx<200){
  12. if(hua[20]>1){
  13. Toast.makeText(MainActivity.this, "请用您的指尖盖住摄像头镜头!", Toast.LENGTH_SHORT).show();
  14. hua[20]=0;}
  15. hua[20] ;
  16. return;}
  17. else
  18. hua[20]=10;
  19. j=0;
  20. }
  21. if(j<20){
  22. addY=hua[j];
  23. j ;
  24. }
  25. //移除数据集中旧的点集
  26. mDataset.removeSeries(series);
  27. //判断当前点集中到底有多少点,因为屏幕总共只能容纳100个,所以当点数超过100时,长度永远是100
  28. int length = series.getItemCount();
  29. int bz=0;
  30. //addX = length;
  31. if (length > 300) {
  32. length = 300;
  33. bz=1;
  34. }
  35. addX = length;
  36. //将旧的点集中x和y的数值取出来放入backup中,并且将x的值加1,造成曲线向右平移的效果
  37. for (int i = 0; i < length; i ) {
  38. xv[i] = (int) series.getX(i) -bz;
  39. yv[i] = (int) series.getY(i);
  40. }
  41. //点集先清空,为了做成新的点集而准备
  42. series.clear();
  43. mDataset.addSeries(series);
  44. //将新产生的点首先加入到点集中,然后在循环体中将坐标变换后的一系列点都重新加入到点集中
  45. //这里可以试验一下把顺序颠倒过来是什么效果,即先运行循环体,再添加新产生的点
  46. series.add(addX, addY);
  47. for (int k = 0; k < length; k ) {
  48. series.add(xv[k], yv[k]);
  49. }
  50. //在数据集中添加新的点集
  51. //mDataset.addSeries(series);
  52. //视图更新,没有这一步,曲线不会呈现动态
  53. //如果在非UI主线程中,需要调用postInvalidate(),具体参考api
  54. chart.invalidate();
  55. } //曲线
(7)相机预览回调方法previewCallback

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

具体代码实现如下:

  1. /**
  2. * 相机预览方法
  3. * 这个方法中实现动态更新界面UI的功能,
  4. * 通过获取手机摄像头的参数来实时动态计算平均像素值、脉冲数,从而实时动态计算心率值。
  5. */
  6. private static PreviewCallback previewCallback = new PreviewCallback() {
  7. public void onPreviewFrame(byte[] data, Camera cam) {
  8. if (data == null)
  9. throw new NullPointerException();
  10. Camera.Size size = cam.getParameters().getPreviewSize();
  11. if (size == null)
  12. throw new NullPointerException();
  13. if (!processing.compareAndSet(false, true))
  14. return;
  15. int width = size.width;
  16. int height = size.height;
  17. //图像处理
  18. int imgAvg = ImageProcessing.decodeYUV420SPtoRedAvg(data.clone(),height,width);
  19. gx=imgAvg;
  20. text1.setText("平均像素值是" String.valueOf(imgAvg));
  21. //像素平均值imgAvg,日志
  22. //Log.i(TAG, "imgAvg=" imgAvg);
  23. if (imgAvg == 0 || imgAvg == 255) {
  24. processing.set(false);
  25. return;
  26. }
  27. //计算平均值
  28. int averageArrayAvg = 0;
  29. int averageArrayCnt = 0;
  30. for (int i = 0; i < averageArray.length; i ) {
  31. if (averageArray[i] > 0) {
  32. averageArrayAvg = averageArray[i];
  33. averageArrayCnt ;
  34. }
  35. }
  36. //计算平均值
  37. int rollingAverage = (averageArrayCnt > 0)?(averageArrayAvg/averageArrayCnt):0;
  38. TYPE newType = currentType;
  39. if (imgAvg < rollingAverage) {
  40. newType = TYPE.RED;
  41. if (newType != currentType) {
  42. beats ;
  43. flag=0;
  44. text2.setText("脉冲数是" String.valueOf(beats));
  45. //Log.e(TAG, "BEAT!! beats=" beats);
  46. }
  47. } else if (imgAvg > rollingAverage) {
  48. newType = TYPE.GREEN;
  49. }
  50. if (averageIndex == averageArraySize)
  51. averageIndex = 0;
  52. averageArray[averageIndex] = imgAvg;
  53. averageIndex ;
  54. // Transitioned from one state to another to the same
  55. if (newType != currentType) {
  56. currentType = newType;
  57. //image.postInvalidate();
  58. }
  59. //获取系统结束时间(ms)
  60. long endTime = System.currentTimeMillis();
  61. double totalTimeInSecs = (endTime - startTime) / 1000d;
  62. if (totalTimeInSecs >= 2) {
  63. double bps = (beats / totalTimeInSecs);
  64. int dpm = (int) (bps * 60d);
  65. if (dpm < 30 || dpm > 180||imgAvg<200) {
  66. //获取系统开始时间(ms)
  67. startTime = System.currentTimeMillis();
  68. //beats心跳总数
  69. beats = 0;
  70. processing.set(false);
  71. return;
  72. }
  73. //Log.e(TAG, "totalTimeInSecs=" totalTimeInSecs " beats=" beats);
  74. if (beatsIndex == beatsArraySize)
  75. beatsIndex = 0;
  76. beatsArray[beatsIndex] = dpm;
  77. beatsIndex ;
  78. int beatsArrayAvg = 0;
  79. int beatsArrayCnt = 0;
  80. for (int i = 0; i < beatsArray.length; i ) {
  81. if (beatsArray[i] > 0) {
  82. beatsArrayAvg = beatsArray[i];
  83. beatsArrayCnt ;
  84. }
  85. }
  86. int beatsAvg = (beatsArrayAvg / beatsArrayCnt);
  87. text.setText("您的的心率是" String.valueOf(beatsAvg) " zhi:" String.valueOf(beatsArray.length)
  88. " " String.valueOf(beatsIndex) " " String.valueOf(beatsArrayAvg) " " String.valueOf(beatsArrayCnt));
  89. //获取系统时间(ms)
  90. startTime = System.currentTimeMillis();
  91. beats = 0;
  92. }
  93. processing.set(false);
  94. }
  95. };
(8)SurfaceHolder.Callback

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

具体代码实现如下:

  1. /**
  2. * 预览回调接口
  3. */
  4. private static SurfaceHolder.Callback surfaceCallback = new SurfaceHolder.Callback() {
  5. //创建时调用
  6. @Override
  7. public void surfaceCreated(SurfaceHolder holder) {
  8. try {
  9. camera.setPreviewDisplay(previewHolder);
  10. camera.setPreviewCallback(previewCallback);
  11. } catch (Throwable t) {
  12. Log.e("PreviewDemo-surfaceCallback","Exception in setPreviewDisplay()", t);
  13. }
  14. }
  15. //当预览改变的时候回调此方法
  16. @Override
  17. public void surfaceChanged(SurfaceHolder holder, int format, int width,int height) {
  18. Camera.Parameters parameters = camera.getParameters();
  19. parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
  20. Camera.Size size = getSmallestPreviewSize(width, height, parameters);
  21. if (size != null) {
  22. parameters.setPreviewSize(size.width, size.height);
  23. // Log.d(TAG, "Using width=" size.width " height=" size.height);
  24. }
  25. camera.setParameters(parameters);
  26. camera.startPreview();
  27. }
  28. //销毁的时候调用
  29. @Override
  30. public void surfaceDestroyed(SurfaceHolder holder) {
  31. // Ignore
  32. }
  33. };
(9)获取相机最小的预览尺寸方法getSmallestPreviewSize

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

具体代码实现如下:

  1. /**
  2. * 获取相机最小的预览尺寸
  3. * @param width
  4. * @param height
  5. * @param parameters
  6. * @return
  7. */
  8. private static Camera.Size getSmallestPreviewSize(int width, int height,
  9. Camera.Parameters parameters) {
  10. Camera.Size result = null;
  11. for (Camera.Size size : parameters.getSupportedPreviewSizes()) {
  12. if (size.width <= width && size.height <= height) {
  13. if (result == null) {
  14. result = size;
  15. } else {
  16. int resultArea = result.width * result.height;
  17. int newArea = size.width * size.height;
  18. if (newArea < resultArea)
  19. result = size;
  20. }
  21. }
  22. }
  23. return result;
  24. }
(10)onCreate方法

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

具体实现代码如下:

  1. @Override
  2. public void onCreate(Bundle savedInstanceState) {
  3. super.onCreate(savedInstanceState);
  4. setContentView(R.layout.activity_main);
  5. initConfig();
  6. }
(11)其他一些Android原生自带方法
  1. @Override
  2. public void onDestroy() {
  3. //当结束程序时关掉Timer
  4. timer.cancel();
  5. super.onDestroy();
  6. };
  7. @Override
  8. public void onConfigurationChanged(Configuration newConfig) {
  9. super.onConfigurationChanged(newConfig);
  10. }
  11. @Override
  12. public void onResume() {
  13. super.onResume();
  14. wakeLock.acquire();
  15. camera = Camera.open();
  16. startTime = System.currentTimeMillis();
  17. }
  18. @Override
  19. public void onPause() {
  20. super.onPause();
  21. wakeLock.release();
  22. camera.setPreviewCallback(null);
  23. camera.stopPreview();
  24. camera.release();
  25. camera = null;
  26. }
(12)MainActivity完整代码

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

  1. package com.lyz.xinlv.activity;
  2. import java.util.Timer;
  3. import java.util.TimerTask;
  4. import java.util.concurrent.atomic.AtomicBoolean;
  5. import org.achartengine.ChartFactory;
  6. import org.achartengine.GraphicalView;
  7. import org.achartengine.chart.PointStyle;
  8. import org.achartengine.model.XYMultipleSeriesDataset;
  9. import org.achartengine.model.XYSeries;
  10. import org.achartengine.renderer.XYMultipleSeriesRenderer;
  11. import org.achartengine.renderer.XYSeriesRenderer;
  12. import android.app.Activity;
  13. import android.content.Context;
  14. import android.content.res.Configuration;
  15. import android.graphics.Color;
  16. import android.graphics.Paint.Align;
  17. import android.hardware.Camera;
  18. import android.hardware.Camera.PreviewCallback;
  19. import android.os.Bundle;
  20. import android.os.Handler;
  21. import android.os.Message;
  22. import android.os.PowerManager;
  23. import android.os.PowerManager.WakeLock;
  24. import android.util.Log;
  25. import android.view.SurfaceHolder;
  26. import android.view.SurfaceView;
  27. import android.view.ViewGroup.LayoutParams;
  28. import android.widget.LinearLayout;
  29. import android.widget.TextView;
  30. import android.widget.Toast;
  31. import com.lyz.monitor.utils.ImageProcessing;
  32. /**
  33. * 程序的主入口
  34. * @author liuyazhuang
  35. *
  36. */
  37. public class MainActivity extends Activity {
  38. //曲线
  39. private Timer timer = new Timer();
  40. //Timer任务,与Timer配套使用
  41. private TimerTask task;
  42. private static int gx;
  43. private static int j;
  44. private static double flag=1;
  45. private Handler handler;
  46. private String title = "pulse";
  47. private XYSeries series;
  48. private XYMultipleSeriesDataset mDataset;
  49. private GraphicalView chart;
  50. private XYMultipleSeriesRenderer renderer;
  51. private Context context;
  52. private int addX = -1;
  53. double addY;
  54. int[] xv = new int[300];
  55. int[] yv = new int[300];
  56. 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};
  57. // private static final String TAG = "HeartRateMonitor";
  58. private static final AtomicBoolean processing = new AtomicBoolean(false);
  59. //Android手机预览控件
  60. private static SurfaceView preview = null;
  61. //预览设置信息
  62. private static SurfaceHolder previewHolder = null;
  63. //Android手机相机句柄
  64. private static Camera camera = null;
  65. //private static View image = null;
  66. private static TextView text = null;
  67. private static TextView text1 = null;
  68. private static TextView text2 = null;
  69. private static WakeLock wakeLock = null;
  70. private static int averageIndex = 0;
  71. private static final int averageArraySize = 4;
  72. private static final int[] averageArray = new int[averageArraySize];
  73. /**
  74. * 类型枚举
  75. * @author liuyazhuang
  76. *
  77. */
  78. public static enum TYPE {
  79. GREEN, RED
  80. };
  81. //设置默认类型
  82. private static TYPE currentType = TYPE.GREEN;
  83. //获取当前类型
  84. public static TYPE getCurrent() {
  85. return currentType;
  86. }
  87. //心跳下标值
  88. private static int beatsIndex = 0;
  89. //心跳数组的大小
  90. private static final int beatsArraySize = 3;
  91. //心跳数组
  92. private static final int[] beatsArray = new int[beatsArraySize];
  93. //心跳脉冲
  94. private static double beats = 0;
  95. //开始时间
  96. private static long startTime = 0;
  97. @Override
  98. public void onCreate(Bundle savedInstanceState) {
  99. super.onCreate(savedInstanceState);
  100. setContentView(R.layout.activity_main);
  101. initConfig();
  102. }
  103. /**
  104. * 初始化配置
  105. */
  106. private void initConfig() {
  107. //曲线
  108. context = getApplicationContext();
  109. //这里获得main界面上的布局,下面会把图表画在这个布局里面
  110. LinearLayout layout = (LinearLayout)findViewById(R.id.linearLayout1);
  111. //这个类用来放置曲线上的所有点,是一个点的集合,根据这些点画出曲线
  112. series = new XYSeries(title);
  113. //创建一个数据集的实例,这个数据集将被用来创建图表
  114. mDataset = new XYMultipleSeriesDataset();
  115. //将点集添加到这个数据集中
  116. mDataset.addSeries(series);
  117. //以下都是曲线的样式和属性等等的设置,renderer相当于一个用来给图表做渲染的句柄
  118. int color = Color.GREEN;
  119. PointStyle style = PointStyle.CIRCLE;
  120. renderer = buildRenderer(color, style, true);
  121. //设置好图表的样式
  122. setChartSettings(renderer, "X", "Y", 0, 300, 4, 16, Color.WHITE, Color.WHITE);
  123. //生成图表
  124. chart = ChartFactory.getLineChartView(context, mDataset, renderer);
  125. //将图表添加到布局中去
  126. layout.addView(chart, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
  127. //这里的Handler实例将配合下面的Timer实例,完成定时更新图表的功能
  128. handler = new Handler() {
  129. @Override
  130. public void handleMessage(Message msg) {
  131. // 刷新图表
  132. updateChart();
  133. super.handleMessage(msg);
  134. }
  135. };
  136. task = new TimerTask() {
  137. @Override
  138. public void run() {
  139. Message message = new Message();
  140. message.what = 1;
  141. handler.sendMessage(message);
  142. }
  143. };
  144. timer.schedule(task, 1,20); //曲线
  145. //获取SurfaceView控件
  146. preview = (SurfaceView) findViewById(R.id.preview);
  147. previewHolder = preview.getHolder();
  148. previewHolder.addCallback(surfaceCallback);
  149. previewHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
  150. // image = findViewById(R.id.image);
  151. text = (TextView) findViewById(R.id.text);
  152. text1 = (TextView) findViewById(R.id.text1);
  153. text2 = (TextView) findViewById(R.id.text2);
  154. PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
  155. wakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, "DoNotDimScreen");
  156. }
  157. // 曲线
  158. @Override
  159. public void onDestroy() {
  160. //当结束程序时关掉Timer
  161. timer.cancel();
  162. super.onDestroy();
  163. };
  164. /**
  165. * 创建图表
  166. * @param color
  167. * @param style
  168. * @param fill
  169. * @return
  170. */
  171. protected XYMultipleSeriesRenderer buildRenderer(int color, PointStyle style, boolean fill) {
  172. XYMultipleSeriesRenderer renderer = new XYMultipleSeriesRenderer();
  173. //设置图表中曲线本身的样式,包括颜色、点的大小以及线的粗细等
  174. XYSeriesRenderer r = new XYSeriesRenderer();
  175. r.setColor(Color.RED);
  176. // r.setPointStyle(null);
  177. // r.setFillPoints(fill);
  178. r.setLineWidth(1);
  179. renderer.addSeriesRenderer(r);
  180. return renderer;
  181. }
  182. /**
  183. * 设置图标的样式
  184. * @param renderer
  185. * @param xTitle:x标题
  186. * @param yTitle:y标题
  187. * @param xMin:x最小长度
  188. * @param xMax:x最大长度
  189. * @param yMin:y最小长度
  190. * @param yMax:y最大长度
  191. * @param axesColor:颜色
  192. * @param labelsColor:标签
  193. */
  194. protected void setChartSettings(XYMultipleSeriesRenderer renderer, String xTitle, String yTitle,
  195. double xMin, double xMax, double yMin, double yMax, int axesColor, int labelsColor) {
  196. //有关对图表的渲染可参看api文档
  197. renderer.setChartTitle(title);
  198. renderer.setXTitle(xTitle);
  199. renderer.setYTitle(yTitle);
  200. renderer.setXAxisMin(xMin);
  201. renderer.setXAxisMax(xMax);
  202. renderer.setYAxisMin(yMin);
  203. renderer.setYAxisMax(yMax);
  204. renderer.setAxesColor(axesColor);
  205. renderer.setLabelsColor(labelsColor);
  206. renderer.setShowGrid(true);
  207. renderer.setGridColor(Color.GREEN);
  208. renderer.setXLabels(20);
  209. renderer.setYLabels(10);
  210. renderer.setXTitle("Time");
  211. renderer.setYTitle("mmHg");
  212. renderer.setYLabelsAlign(Align.RIGHT);
  213. renderer.setPointSize((float) 3 );
  214. renderer.setShowLegend(false);
  215. }
  216. /**
  217. * 更新图标信息
  218. */
  219. private void updateChart() {
  220. //设置好下一个需要增加的节点
  221. if(flag==1)
  222. addY=10;
  223. else{
  224. // addY=250;
  225. flag=1;
  226. if(gx<200){
  227. if(hua[20]>1){
  228. Toast.makeText(MainActivity.this, "请用您的指尖盖住摄像头镜头!", Toast.LENGTH_SHORT).show();
  229. hua[20]=0;}
  230. hua[20] ;
  231. return;}
  232. else
  233. hua[20]=10;
  234. j=0;
  235. }
  236. if(j<20){
  237. addY=hua[j];
  238. j ;
  239. }
  240. //移除数据集中旧的点集
  241. mDataset.removeSeries(series);
  242. //判断当前点集中到底有多少点,因为屏幕总共只能容纳100个,所以当点数超过100时,长度永远是100
  243. int length = series.getItemCount();
  244. int bz=0;
  245. //addX = length;
  246. if (length > 300) {
  247. length = 300;
  248. bz=1;
  249. }
  250. addX = length;
  251. //将旧的点集中x和y的数值取出来放入backup中,并且将x的值加1,造成曲线向右平移的效果
  252. for (int i = 0; i < length; i ) {
  253. xv[i] = (int) series.getX(i) -bz;
  254. yv[i] = (int) series.getY(i);
  255. }
  256. //点集先清空,为了做成新的点集而准备
  257. series.clear();
  258. mDataset.addSeries(series);
  259. //将新产生的点首先加入到点集中,然后在循环体中将坐标变换后的一系列点都重新加入到点集中
  260. //这里可以试验一下把顺序颠倒过来是什么效果,即先运行循环体,再添加新产生的点
  261. series.add(addX, addY);
  262. for (int k = 0; k < length; k ) {
  263. series.add(xv[k], yv[k]);
  264. }
  265. //在数据集中添加新的点集
  266. //mDataset.addSeries(series);
  267. //视图更新,没有这一步,曲线不会呈现动态
  268. //如果在非UI主线程中,需要调用postInvalidate(),具体参考api
  269. chart.invalidate();
  270. } //曲线
  271. @Override
  272. public void onConfigurationChanged(Configuration newConfig) {
  273. super.onConfigurationChanged(newConfig);
  274. }
  275. @Override
  276. public void onResume() {
  277. super.onResume();
  278. wakeLock.acquire();
  279. camera = Camera.open();
  280. startTime = System.currentTimeMillis();
  281. }
  282. @Override
  283. public void onPause() {
  284. super.onPause();
  285. wakeLock.release();
  286. camera.setPreviewCallback(null);
  287. camera.stopPreview();
  288. camera.release();
  289. camera = null;
  290. }
  291. /**
  292. * 相机预览方法
  293. * 这个方法中实现动态更新界面UI的功能,
  294. * 通过获取手机摄像头的参数来实时动态计算平均像素值、脉冲数,从而实时动态计算心率值。
  295. */
  296. private static PreviewCallback previewCallback = new PreviewCallback() {
  297. public void onPreviewFrame(byte[] data, Camera cam) {
  298. if (data == null)
  299. throw new NullPointerException();
  300. Camera.Size size = cam.getParameters().getPreviewSize();
  301. if (size == null)
  302. throw new NullPointerException();
  303. if (!processing.compareAndSet(false, true))
  304. return;
  305. int width = size.width;
  306. int height = size.height;
  307. //图像处理
  308. int imgAvg = ImageProcessing.decodeYUV420SPtoRedAvg(data.clone(),height,width);
  309. gx=imgAvg;
  310. text1.setText("平均像素值是" String.valueOf(imgAvg));
  311. //像素平均值imgAvg,日志
  312. //Log.i(TAG, "imgAvg=" imgAvg);
  313. if (imgAvg == 0 || imgAvg == 255) {
  314. processing.set(false);
  315. return;
  316. }
  317. //计算平均值
  318. int averageArrayAvg = 0;
  319. int averageArrayCnt = 0;
  320. for (int i = 0; i < averageArray.length; i ) {
  321. if (averageArray[i] > 0) {
  322. averageArrayAvg = averageArray[i];
  323. averageArrayCnt ;
  324. }
  325. }
  326. //计算平均值
  327. int rollingAverage = (averageArrayCnt > 0)?(averageArrayAvg/averageArrayCnt):0;
  328. TYPE newType = currentType;
  329. if (imgAvg < rollingAverage) {
  330. newType = TYPE.RED;
  331. if (newType != currentType) {
  332. beats ;
  333. flag=0;
  334. text2.setText("脉冲数是" String.valueOf(beats));
  335. //Log.e(TAG, "BEAT!! beats=" beats);
  336. }
  337. } else if (imgAvg > rollingAverage) {
  338. newType = TYPE.GREEN;
  339. }
  340. if (averageIndex == averageArraySize)
  341. averageIndex = 0;
  342. averageArray[averageIndex] = imgAvg;
  343. averageIndex ;
  344. // Transitioned from one state to another to the same
  345. if (newType != currentType) {
  346. currentType = newType;
  347. //image.postInvalidate();
  348. }
  349. //获取系统结束时间(ms)
  350. long endTime = System.currentTimeMillis();
  351. double totalTimeInSecs = (endTime - startTime) / 1000d;
  352. if (totalTimeInSecs >= 2) {
  353. double bps = (beats / totalTimeInSecs);
  354. int dpm = (int) (bps * 60d);
  355. if (dpm < 30 || dpm > 180||imgAvg<200) {
  356. //获取系统开始时间(ms)
  357. startTime = System.currentTimeMillis();
  358. //beats心跳总数
  359. beats = 0;
  360. processing.set(false);
  361. return;
  362. }
  363. //Log.e(TAG, "totalTimeInSecs=" totalTimeInSecs " beats=" beats);
  364. if (beatsIndex == beatsArraySize)
  365. beatsIndex = 0;
  366. beatsArray[beatsIndex] = dpm;
  367. beatsIndex ;
  368. int beatsArrayAvg = 0;
  369. int beatsArrayCnt = 0;
  370. for (int i = 0; i < beatsArray.length; i ) {
  371. if (beatsArray[i] > 0) {
  372. beatsArrayAvg = beatsArray[i];
  373. beatsArrayCnt ;
  374. }
  375. }
  376. int beatsAvg = (beatsArrayAvg / beatsArrayCnt);
  377. text.setText("您的的心率是" String.valueOf(beatsAvg) " zhi:" String.valueOf(beatsArray.length)
  378. " " String.valueOf(beatsIndex) " " String.valueOf(beatsArrayAvg) " " String.valueOf(beatsArrayCnt));
  379. //获取系统时间(ms)
  380. startTime = System.currentTimeMillis();
  381. beats = 0;
  382. }
  383. processing.set(false);
  384. }
  385. };
  386. /**
  387. * 预览回调接口
  388. */
  389. private static SurfaceHolder.Callback surfaceCallback = new SurfaceHolder.Callback() {
  390. //创建时调用
  391. @Override
  392. public void surfaceCreated(SurfaceHolder holder) {
  393. try {
  394. camera.setPreviewDisplay(previewHolder);
  395. camera.setPreviewCallback(previewCallback);
  396. } catch (Throwable t) {
  397. Log.e("PreviewDemo-surfaceCallback","Exception in setPreviewDisplay()", t);
  398. }
  399. }
  400. //当预览改变的时候回调此方法
  401. @Override
  402. public void surfaceChanged(SurfaceHolder holder, int format, int width,int height) {
  403. Camera.Parameters parameters = camera.getParameters();
  404. parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
  405. Camera.Size size = getSmallestPreviewSize(width, height, parameters);
  406. if (size != null) {
  407. parameters.setPreviewSize(size.width, size.height);
  408. // Log.d(TAG, "Using width=" size.width " height=" size.height);
  409. }
  410. camera.setParameters(parameters);
  411. camera.startPreview();
  412. }
  413. //销毁的时候调用
  414. @Override
  415. public void surfaceDestroyed(SurfaceHolder holder) {
  416. // Ignore
  417. }
  418. };
  419. /**
  420. * 获取相机最小的预览尺寸
  421. * @param width
  422. * @param height
  423. * @param parameters
  424. * @return
  425. */
  426. private static Camera.Size getSmallestPreviewSize(int width, int height,
  427. Camera.Parameters parameters) {
  428. Camera.Size result = null;
  429. for (Camera.Size size : parameters.getSupportedPreviewSizes()) {
  430. if (size.width <= width && size.height <= height) {
  431. if (result == null) {
  432. result = size;
  433. } else {
  434. int resultArea = result.width * result.height;
  435. int newArea = size.width * size.height;
  436. if (newArea < resultArea)
  437. result = size;
  438. }
  439. }
  440. }
  441. return result;
  442. }
  443. }

3、UI布局

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

具体代码实现如下:

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. xmlns:tools="http://schemas.android.com/tools"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent"
  5. android:orientation="vertical" >
  6. <SurfaceView
  7. android:id="@ id/preview"
  8. android:layout_width="fill_parent"
  9. android:layout_height="200dp"
  10. android:layout_marginLeft="50dip"
  11. android:layout_marginRight="50dip" />
  12. <LinearLayout
  13. android:id="@ id/linearLayout1"
  14. android:layout_width="match_parent"
  15. android:layout_height="200dp"
  16. android:orientation="vertical" >
  17. </LinearLayout>
  18. <TextView
  19. android:id="@ id/text"
  20. android:layout_width="wrap_content"
  21. android:layout_height="wrap_content"
  22. android:layout_marginLeft="50dip"
  23. android:layout_weight="1"
  24. android:text="@string/show" >
  25. </TextView>
  26. <TextView
  27. android:id="@ id/text1"
  28. android:layout_width="wrap_content"
  29. android:layout_height="wrap_content"
  30. android:layout_marginLeft="50dip"
  31. android:layout_weight="1"
  32. android:text="@string/show" >
  33. </TextView>
  34. <TextView
  35. android:id="@ id/text2"
  36. android:layout_width="wrap_content"
  37. android:layout_height="wrap_content"
  38. android:layout_marginLeft="50dip"
  39. android:layout_weight="1"
  40. android:text="@string/show" >
  41. </TextView>
  42. </LinearLayout>

4、strings.xml

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <resources>
  3. <string name="app_name">心率检测</string>
  4. <string name="action_settings">Settings</string>
  5. <string name="hello_world">Hello world!</string>
  6. <string name="show">显示</string>
  7. </resources>

5、授权AndroidManifest.xml

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

具体代码实现如下:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <manifest xmlns:android="http://schemas.android.com/apk/res/android"
  3. package="com.lyz.xinlv.activity"
  4. android:versionCode="1"
  5. android:versionName="1.0" >
  6. <uses-sdk
  7. android:minSdkVersion="15"
  8. android:targetSdkVersion="15" />
  9. <uses-permission android:name="android.permission.WAKE_LOCK" />
  10. <uses-permission android:name="android.permission.CAMERA" />
  11. <uses-feature android:name="android.hardware.camera" />
  12. <uses-feature android:name="android.hardware.camera.autofocus" />
  13. <application
  14. android:allowBackup="true"
  15. android:icon="@drawable/ic_launcher"
  16. android:label="@string/app_name"
  17. android:theme="@style/AppTheme" >
  18. <activity
  19. android:name="com.lyz.xinlv.activity.MainActivity"
  20. android:label="@string/app_name" >
  21. <intent-filter>
  22. <action android:name="android.intent.action.MAIN" />
  23. <category android:name="android.intent.category.LAUNCHER" />
  24. </intent-filter>
  25. </activity>
  26. </application>
  27. </manifest>

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

三、运行效果

四、温馨提示

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

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

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

  1. 基于android的检测心率,基于android手机的血氧饱和度检测

    67 基于 android手机的血氧饱和度检测 许方成,赵曙光,杨 峰,黄佳佳 (东华大学信息科学与技术学院,上海 201620) 摘要:血氧饱和度是人体健康状况的标准指标,连续记录血氧饱和度可以预测 ...

  2. Android内存泄漏的检测流程、捕捉以及分析

    https://blog.csdn.net/qq_20280683/article/details/77964208 Android内存泄漏的检测流程.捕捉以及分析 简述: 一个APP的性能,重度关乎 ...

  3. 心跳之旅—iOS用手机摄像头检测心率(PPG)

    本文中涉及到的要点主要有: AVCapture Core Graphics Delegate & Block RGB -> HSV 带通滤波 基音标注算法(TP-Psola) 光电容积脉 ...

  4. 基于STM32F103RB的模拟液位检测告警系统(已实现)

    这次先看一下老朋友STM32,有关Zigbee的内容(Z-stack协议栈和基于Zigbee的环境温湿度+烟雾传感器的环境监测系统),等有时间再更.基于STM32的模拟液位检测告警系统,记得好像是某一 ...

  5. 主机模拟i2c检测设备时出现错误死循环_适用于PLC/DCS应用,支持HART和Modbus连接的模拟I/O系统...

    评估和设计支持 电路评估板 CN-0414电路评估板(EVAL-CN0414-ARDZ) CN-0418电路评估板(EVAL-CN0418-ARDZ) CN-0416电路评估板(EVAL-CN0416 ...

  6. 【STM32G4】备战蓝桥杯嵌入式---实战---第七届嵌入式省赛—“模拟液位检测告警系统”

    文章目录 前言 一.题目 二.模块初始化以及功能分析 1.模块的初始化 2.模块功能分析 三.函数实现 1.void Display(void); 2.void EEPROM_Read(void);v ...

  7. 蓝桥杯嵌入式STM32G431——第七届省赛真题模拟液位检测告警系统

    第七届省赛真题模拟液位检测告警系统 第七届省赛真题 主函数部分的代码功能实现(不包含各模块初始化代码) 第七届省赛真题 主函数部分的代码功能实现(不包含各模块初始化代码) #include " ...

  8. 蓝桥杯嵌入式第七届省赛——“模拟液位检测告警系统”旧板标准库

    一.赛题分析 模拟液位检测告警系统,涉及到KEY.ADC.EEPROM.LED.USART.LCD等模块.本届赛题相对简单,各个模块均可在往期文章中找到配置方法和编写说明.如: 蓝桥杯嵌入式--第六届 ...

  9. android 检测过程,Android 系统对permision的检测过程

    Android 系统对permision的检测过程 RK3288 5.1 中以太网设置静态IP 对permission的检测 简略的调用过程如下: frameworks\opt\net\etherne ...

最新文章

  1. Ubuntu 系统下终端界面在打开一个终端的快捷键
  2. 打造Worktile敏捷开发管理工具的思与惑
  3. 深入理解面向对象 -- 基于 JavaScript 实现
  4. springmvc二十七:springmvc-ResponseBody与ResponseEntity
  5. jQuery EasyUI API 中文文档 - Documentation 文档
  6. Atom编写Markdown
  7. C# 实验四 修改版 获取系统时间、点击加一秒功能
  8. python入门第三天
  9. 手写数字识别全部代码--全连接神经网络方法
  10. 中国搜索 20 年:易守难攻、刚需不减!
  11. [FreeProxy]FreeProxy代理服务器端软件介绍 之一
  12. layui报错 “Layui hint: 模块名 xxx 已被占用“ 的问题解决方案
  13. 状态模式(Strategy Pattern)
  14. Here we are 团队简介
  15. python 基础 5 while循环语句
  16. java正则过滤小括号,java正则表达式获取大括号小括号内容并判断数字和小数亲测可用...
  17. Java面试必看的18个开源项目
  18. 【OR】YALMIP大M法和凸包
  19. Magisk升级Zygisk
  20. 注册一个域名需要多少钱_域名注册需要多少钱?注册一个域名一般要多少钱?贵吗?...

热门文章

  1. asp动态网站编程课程体系
  2. python数据分析入门详解!!!非常详细!!!
  3. 计算机主机装配视频,预算一万如何组装电脑?新媒体视频剪辑等影视专业装机指南2020.8...
  4. 【AAAI 2020】全部接受论文列表(三)
  5. BeanFactory和ApplicationContext接口的联系和区别
  6. C# 数组内元素合并转换成以指定字符连接的字符串
  7. 小米8 se图片备份到电脑上
  8. 成功与失败——《异类》读后感
  9. “偷梁换柱”的库打桩机制
  10. android arcgis定位,arcgis android之定位功能的示例代码