参考博客:https://blog.csdn.net/qq_36982160/article/details/81260273
参考github:https://github.com/ZhaoYukai/HeartRate
如果运行时出现Program type already present: android.support.v4.app.BackStackRecord$Op错误,参考:https://stackoverflow.com/questions/49917614/program-type-already-present-android-support-v4-app-backstackrecordop
首先膜拜以上大神的博客和github,本人在引用上方github项目时出现了些问题,所以记下来以备以后用到。先讲下改错的过程:本人在Android Studio新建了项目后,就把github上的代码粘贴了过来,然后在manifests删掉以下内容:
<uses-sdk
android:minSdkVersion="14"
android:targetSdkVersion="19" />
然后把github项目中的HeartRate-masterHeartRate-masterlibs路径里的jar包复制到了AS模块里的libs路径下(后面看来android-support-v4.jar包用不到),选中后右键添加到库。然后在模块级的build.gradle中修改compileSdkVersion、targetSdkVersion为27(https://www.jianshu.com/p/808e1d127a33)。然后修改了两个implementation 如下:
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support:support-v4:27.1.1'
然后修改MainActivity的几个地方如下:
wakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, "DoNotDimScreen"); ====》
wakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, "myapp:wakeLock");
然后在运行app前授权允许使用照相机后即可运行app
运行效果图:
模块结构图:
模块级的build.gradle:
1 apply plugin: 'com.android.application' 2 3 android { 4 compileSdkVersion 27 5 6 7 8 defaultConfig { 9 applicationId "com.mingrisoft.heartdetect" 10 minSdkVersion 15 11 targetSdkVersion 27 12 versionCode 1 13 versionName "1.0" 14 15 testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 16 17 } 18 19 buildTypes { 20 release { 21 minifyEnabled false 22 proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 23 } 24 } 25 26 } 27 28 dependencies { 29 implementation fileTree(include: ['*.jar'], dir: 'libs') 30 implementation 'com.android.support:appcompat-v7:27.1.1' 31 implementation 'com.android.support:support-v4:27.1.1' 32 implementation 'com.android.support.constraint:constraint-layout:1.1.3' 33 testImplementation 'junit:junit:4.12' 34 androidTestImplementation 'com.android.support.test:runner:1.0.2' 35 androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 36 implementation files('libs/achartengine-1.0.0.jar') 37 }
manifests:
1 <?xml version="1.0" encoding="utf-8"?> 2 <manifest xmlns:android="http://schemas.android.com/apk/res/android" 3 package="com.mingrisoft.heartdetect"> 4 5 <uses-permission android:name="android.permission.WAKE_LOCK" /> 6 <uses-permission android:name="android.permission.CAMERA" /> 7 <uses-feature android:name="android.hardware.camera" /> 8 <uses-feature android:name="android.hardware.camera.autofocus" /> 9 10 <application 11 android:allowBackup="true" 12 android:icon="@mipmap/ic_launcher" 13 android:label="@string/app_name" 14 android:roundIcon="@mipmap/ic_launcher_round" 15 android:supportsRtl="true" 16 android:theme="@style/AppTheme"> 17 <activity android:name=".MainActivity"> 18 <intent-filter> 19 <action android:name="android.intent.action.MAIN" /> 20 21 <category android:name="android.intent.category.LAUNCHER" /> 22 </intent-filter> 23 </activity> 24 </application> 25 26 </manifest>
strings.xml:
1 <resources> 2 <string name="app_name">heartDetect</string> 3 <string name="hello_world">Hello world!</string> 4 <string name="action_settings">Settings</string> 5 <string name="show">显示</string> 6 7 </resources>
activity_main.xml:
1 <?xml version="1.0" encoding="utf-8"?> 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 xmlns:app="http://schemas.android.com/apk/res-auto" 4 xmlns:tools="http://schemas.android.com/tools" 5 android:layout_width="match_parent" 6 android:layout_height="match_parent" 7 android:orientation="vertical" 8 tools:context=".MainActivity"> 9 10 <SurfaceView 11 android:id="@+id/id_preview" 12 android:layout_width="match_parent" 13 android:layout_height="200dp" 14 android:layout_marginLeft="50dp" 15 android:layout_marginRight="50dp" /> 16 17 <LinearLayout 18 android:id="@+id/id_linearLayout_graph" 19 android:layout_width="match_parent" 20 android:layout_height="200dp" 21 android:orientation="vertical" > 22 </LinearLayout> 23 24 <TextView 25 android:id="@+id/id_tv_heart_rate" 26 android:layout_width="wrap_content" 27 android:layout_height="wrap_content" 28 android:layout_marginLeft="50dp" 29 android:layout_weight="1" 30 android:text="@string/show" > 31 </TextView> 32 33 <TextView 34 android:id="@+id/id_tv_Avg_Pixel_Values" 35 android:layout_width="wrap_content" 36 android:layout_height="wrap_content" 37 android:layout_marginLeft="50dp" 38 android:layout_weight="1" 39 android:text="@string/show" > 40 </TextView> 41 42 <TextView 43 android:id="@+id/id_tv_pulse" 44 android:layout_width="wrap_content" 45 android:layout_height="wrap_content" 46 android:layout_marginLeft="50dp" 47 android:layout_weight="1" 48 android:text="@string/show" > 49 </TextView> 50 51 52 </LinearLayout >
ImageProcessing:
1 package com.mingrisoft.heartdetect; 2 3 /** 4 * 图像处理类 5 */ 6 public abstract class ImageProcessing { 7 8 /** 9 * 内部调用的处理图片的方法 10 */ 11 private static int decodeYUV420SPtoRedSum(byte[] yuv420sp , int width , int height) { 12 if (yuv420sp == null) { 13 return 0; 14 } 15 16 final int frameSize = width * height; 17 int sum = 0; 18 19 for (int j = 0 , yp = 0 ; j < height ; j++) { 20 int uvp = frameSize + (j >> 1) * width; 21 int u = 0; 22 int v = 0; 23 for (int i = 0 ; i < width ; i++, yp++) { 24 int y = (0xff & ((int) yuv420sp[yp])) - 16; 25 if (y < 0) { 26 y = 0; 27 } 28 if ((i & 1) == 0) { 29 v = (0xff & yuv420sp[uvp++]) - 128; 30 u = (0xff & yuv420sp[uvp++]) - 128; 31 } 32 int y1192 = 1192 * y; 33 int r = (y1192 + 1634 * v); 34 int g = (y1192 - 833 * v - 400 * u); 35 int b = (y1192 + 2066 * u); 36 37 if (r < 0) { 38 r = 0; 39 } 40 else if (r > 262143) { 41 r = 262143; 42 } 43 44 if (g < 0) { 45 g = 0; 46 } 47 else if (g > 262143) { 48 g = 262143; 49 } 50 51 if (b < 0) { 52 b = 0; 53 } 54 else if (b > 262143) { 55 b = 262143; 56 } 57 58 int pixel = 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) & 0xff00) | ((b >> 10) & 0xff); 59 int red = (pixel >> 16) & 0xff; 60 sum += red; 61 } 62 } 63 return sum; 64 } 65 66 /** 67 * 对外开放的图像处理方法 68 */ 69 public static int decodeYUV420SPtoRedAvg(byte[] yuv420sp , int width , int height) { 70 if (yuv420sp == null) { 71 return 0; 72 } 73 final int frameSize = width * height; 74 int sum = decodeYUV420SPtoRedSum(yuv420sp, width, height); 75 return (sum / frameSize); 76 } 77 }
MainActivity:
1 package com.mingrisoft.heartdetect; 2 3 import android.app.Activity; 4 import android.content.Context; 5 import android.content.res.Configuration; 6 import android.graphics.Color; 7 import android.graphics.Paint.Align; 8 import android.hardware.Camera; 9 import android.hardware.Camera.PreviewCallback; 10 import android.os.Bundle; 11 import android.os.Handler; 12 import android.os.Message; 13 import android.os.PowerManager; 14 import android.os.PowerManager.WakeLock; 15 import android.util.Log; 16 import android.view.SurfaceHolder; 17 import android.view.SurfaceView; 18 import android.view.ViewGroup.LayoutParams; 19 import android.widget.LinearLayout; 20 import android.widget.TextView; 21 import android.widget.Toast; 22 23 import org.achartengine.ChartFactory; 24 import org.achartengine.GraphicalView; 25 import org.achartengine.chart.PointStyle; 26 import org.achartengine.model.XYMultipleSeriesDataset; 27 import org.achartengine.model.XYSeries; 28 import org.achartengine.renderer.XYMultipleSeriesRenderer; 29 import org.achartengine.renderer.XYSeriesRenderer; 30 31 import java.util.Timer; 32 import java.util.TimerTask; 33 import java.util.concurrent.atomic.AtomicBoolean; 34 35 /** 36 * 程序的主入口 37 */ 38 public class MainActivity extends Activity { 39 //曲线 40 private Timer timer = new Timer(); 41 //Timer任务,与Timer配套使用 42 private TimerTask task; 43 private static int gx; 44 private static int j; 45 46 private static double flag = 1; 47 private Handler handler; 48 private String title = "pulse"; 49 private XYSeries series; 50 private XYMultipleSeriesDataset mDataset; 51 private GraphicalView chart; 52 private XYMultipleSeriesRenderer renderer; 53 private Context context; 54 private int addX = -1; 55 double addY; 56 int[] xv = new int[300]; 57 int[] yv = new int[300]; 58 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}; 59 60 private static final AtomicBoolean processing = new AtomicBoolean(false); 61 //Android手机预览控件 62 private static SurfaceView preview = null; 63 //预览设置信息 64 private static SurfaceHolder previewHolder = null; 65 //Android手机相机句柄 66 private static Camera camera = null; 67 //private static View image = null; 68 private static TextView mTV_Heart_Rate = null; 69 private static TextView mTV_Avg_Pixel_Values = null; 70 private static TextView mTV_pulse = null; 71 private static WakeLock wakeLock = null; 72 private static int averageIndex = 0; 73 private static final int averageArraySize = 4; 74 private static final int[] averageArray = new int[averageArraySize]; 75 76 /** 77 * 类型枚举 78 */ 79 public static enum TYPE { 80 GREEN, RED 81 }; 82 83 //设置默认类型 84 private static TYPE currentType = TYPE.GREEN; 85 //获取当前类型 86 public static TYPE getCurrent() { 87 return currentType; 88 } 89 //心跳下标值 90 private static int beatsIndex = 0; 91 //心跳数组的大小 92 private static final int beatsArraySize = 3; 93 //心跳数组 94 private static final int[] beatsArray = new int[beatsArraySize]; 95 //心跳脉冲 96 private static double beats = 0; 97 //开始时间 98 private static long startTime = 0; 99 100 101 102 @Override 103 public void onCreate(Bundle savedInstanceState) { 104 super.onCreate(savedInstanceState); 105 setContentView(R.layout.activity_main); //这里注意要改成自己的布局文件 106 107 initConfig(); 108 } 109 110 /** 111 * 初始化配置 112 */ 113 @SuppressWarnings("deprecation") 114 private void initConfig() { 115 //曲线 116 context = getApplicationContext(); 117 118 //这里获得main界面上的布局,下面会把图表画在这个布局里面 119 LinearLayout layout = (LinearLayout)findViewById(R.id.id_linearLayout_graph); 120 121 //这个类用来放置曲线上的所有点,是一个点的集合,根据这些点画出曲线 122 series = new XYSeries(title); 123 124 //创建一个数据集的实例,这个数据集将被用来创建图表 125 mDataset = new XYMultipleSeriesDataset(); 126 127 //将点集添加到这个数据集中 128 mDataset.addSeries(series); 129 130 //以下都是曲线的样式和属性等等的设置,renderer相当于一个用来给图表做渲染的句柄 131 int color = Color.GREEN; 132 PointStyle style = PointStyle.CIRCLE; 133 renderer = buildRenderer(color, style, true); 134 135 //设置好图表的样式 136 setChartSettings(renderer, "X", "Y", 0, 300, 4, 16, Color.WHITE, Color.WHITE); 137 138 //生成图表 139 chart = ChartFactory.getLineChartView(context, mDataset, renderer); 140 141 //将图表添加到布局中去 142 layout.addView(chart, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); 143 144 //这里的Handler实例将配合下面的Timer实例,完成定时更新图表的功能 145 handler = new Handler() { 146 @Override 147 public void handleMessage(Message msg) { 148 //刷新图表 149 updateChart(); 150 super.handleMessage(msg); 151 } 152 }; 153 154 task = new TimerTask() { 155 @Override 156 public void run() { 157 Message message = new Message(); 158 message.what = 1; 159 handler.sendMessage(message); 160 } 161 }; 162 163 timer.schedule(task, 1,20); //曲线 164 //获取SurfaceView控件 165 preview = (SurfaceView) findViewById(R.id.id_preview); 166 previewHolder = preview.getHolder(); 167 previewHolder.addCallback(surfaceCallback); 168 previewHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); 169 170 mTV_Heart_Rate = (TextView) findViewById(R.id.id_tv_heart_rate); 171 mTV_Avg_Pixel_Values = (TextView) findViewById(R.id.id_tv_Avg_Pixel_Values); 172 mTV_pulse = (TextView) findViewById(R.id.id_tv_pulse); 173 174 PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); 175 wakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, "myapp:wakeLock"); 176 } 177 178 // 曲线 179 @Override 180 public void onDestroy() { 181 //当结束程序时关掉Timer 182 timer.cancel(); 183 super.onDestroy(); 184 }; 185 186 /** 187 * 创建图表 188 */ 189 protected XYMultipleSeriesRenderer buildRenderer(int color, PointStyle style, boolean fill) { 190 XYMultipleSeriesRenderer renderer = new XYMultipleSeriesRenderer(); 191 192 //设置图表中曲线本身的样式,包括颜色、点的大小以及线的粗细等 193 XYSeriesRenderer r = new XYSeriesRenderer(); 194 r.setColor(Color.RED); 195 r.setLineWidth(1); 196 renderer.addSeriesRenderer(r); 197 return renderer; 198 } 199 200 /** 201 * 设置图标的样式 202 * @param renderer 203 * @param xTitle:x标题 204 * @param yTitle:y标题 205 * @param xMin:x最小长度 206 * @param xMax:x最大长度 207 * @param yMin:y最小长度 208 * @param yMax:y最大长度 209 * @param axesColor:颜色 210 * @param labelsColor:标签 211 */ 212 protected void setChartSettings(XYMultipleSeriesRenderer renderer, String xTitle, String yTitle, 213 double xMin, double xMax, double yMin, double yMax, int axesColor, int labelsColor) { 214 //有关对图表的渲染可参看api文档 215 renderer.setChartTitle(title); 216 renderer.setXTitle(xTitle); 217 renderer.setYTitle(yTitle); 218 renderer.setXAxisMin(xMin); 219 renderer.setXAxisMax(xMax); 220 renderer.setYAxisMin(yMin); 221 renderer.setYAxisMax(yMax); 222 renderer.setAxesColor(axesColor); 223 renderer.setLabelsColor(labelsColor); 224 renderer.setShowGrid(true); 225 renderer.setGridColor(Color.GREEN); 226 renderer.setXLabels(20); 227 renderer.setYLabels(10); 228 renderer.setXTitle("Time"); 229 renderer.setYTitle("mmHg"); 230 renderer.setYLabelsAlign(Align.RIGHT); 231 renderer.setPointSize((float) 3 ); 232 renderer.setShowLegend(false); 233 } 234 235 /** 236 * 更新图标信息 237 */ 238 private void updateChart() { 239 //设置好下一个需要增加的节点 240 if(flag == 1) { 241 addY = 10; 242 } 243 else { 244 flag = 1; 245 if(gx < 200){ 246 if(hua[20] > 1){ 247 Toast.makeText(MainActivity.this, "请用您的指尖盖住摄像头镜头!", Toast.LENGTH_SHORT).show(); 248 hua[20] = 0; 249 } 250 hua[20]++; 251 return; 252 } 253 else { 254 hua[20] = 10; 255 } 256 j = 0; 257 } 258 if(j < 20){ 259 addY=hua[j]; 260 j++; 261 } 262 263 //移除数据集中旧的点集 264 mDataset.removeSeries(series); 265 266 //判断当前点集中到底有多少点,因为屏幕总共只能容纳100个,所以当点数超过100时,长度永远是100 267 int length = series.getItemCount(); 268 int bz = 0; 269 //addX = length; 270 if (length > 300) { 271 length = 300; 272 bz=1; 273 } 274 addX = length; 275 //将旧的点集中x和y的数值取出来放入backup中,并且将x的值加1,造成曲线向右平移的效果 276 for (int i = 0; i < length; i++) { 277 xv[i] = (int) series.getX(i) - bz; 278 yv[i] = (int) series.getY(i); 279 } 280 281 //点集先清空,为了做成新的点集而准备 282 series.clear(); 283 mDataset.addSeries(series); 284 //将新产生的点首先加入到点集中,然后在循环体中将坐标变换后的一系列点都重新加入到点集中 285 //这里可以试验一下把顺序颠倒过来是什么效果,即先运行循环体,再添加新产生的点 286 series.add(addX, addY); 287 for (int k = 0; k < length; k++) { 288 series.add(xv[k], yv[k]); 289 } 290 //在数据集中添加新的点集 291 //mDataset.addSeries(series); 292 293 //视图更新,没有这一步,曲线不会呈现动态 294 //如果在非UI主线程中,需要调用postInvalidate(),具体参考api 295 chart.invalidate(); 296 } //曲线 297 298 299 @Override 300 public void onConfigurationChanged(Configuration newConfig) { 301 super.onConfigurationChanged(newConfig); 302 } 303 304 @Override 305 public void onResume() { 306 super.onResume(); 307 wakeLock.acquire(); 308 camera = Camera.open(); 309 startTime = System.currentTimeMillis(); 310 } 311 312 @Override 313 public void onPause() { 314 super.onPause(); 315 wakeLock.release(); 316 camera.setPreviewCallback(null); 317 camera.stopPreview(); 318 camera.release(); 319 camera = null; 320 } 321 322 323 /** 324 * 相机预览方法 325 * 这个方法中实现动态更新界面UI的功能, 326 * 通过获取手机摄像头的参数来实时动态计算平均像素值、脉冲数,从而实时动态计算心率值。 327 */ 328 private static PreviewCallback previewCallback = new PreviewCallback() { 329 public void onPreviewFrame(byte[] data, Camera cam) { 330 if (data == null) { 331 throw new NullPointerException(); 332 } 333 Camera.Size size = cam.getParameters().getPreviewSize(); 334 if (size == null) { 335 throw new NullPointerException(); 336 } 337 if (!processing.compareAndSet(false, true)) { 338 return; 339 } 340 int width = size.width; 341 int height = size.height; 342 343 //图像处理 344 int imgAvg = ImageProcessing.decodeYUV420SPtoRedAvg(data.clone(),height,width); 345 gx = imgAvg; 346 mTV_Avg_Pixel_Values.setText("平均像素值是" + String.valueOf(imgAvg)); 347 348 if (imgAvg == 0 || imgAvg == 255) { 349 processing.set(false); 350 return; 351 } 352 //计算平均值 353 int averageArrayAvg = 0; 354 int averageArrayCnt = 0; 355 for (int i = 0; i < averageArray.length; i++) { 356 if (averageArray[i] > 0) { 357 averageArrayAvg += averageArray[i]; 358 averageArrayCnt++; 359 } 360 } 361 362 //计算平均值 363 int rollingAverage = (averageArrayCnt > 0)?(averageArrayAvg/averageArrayCnt):0; 364 TYPE newType = currentType; 365 if (imgAvg < rollingAverage) { 366 newType = TYPE.RED; 367 if (newType != currentType) { 368 beats++; 369 flag=0; 370 mTV_pulse.setText("脉冲数是" + String.valueOf(beats)); 371 } 372 } else if (imgAvg > rollingAverage) { 373 newType = TYPE.GREEN; 374 } 375 376 if(averageIndex == averageArraySize) { 377 averageIndex = 0; 378 } 379 averageArray[averageIndex] = imgAvg; 380 averageIndex++; 381 382 if (newType != currentType) { 383 currentType = newType; 384 } 385 386 //获取系统结束时间(ms) 387 long endTime = System.currentTimeMillis(); 388 double totalTimeInSecs = (endTime - startTime) / 1000d; 389 if (totalTimeInSecs >= 2) { 390 double bps = (beats / totalTimeInSecs); 391 int dpm = (int) (bps * 60d); 392 if (dpm < 30 || dpm > 180|| imgAvg < 200) { 393 //获取系统开始时间(ms) 394 startTime = System.currentTimeMillis(); 395 //beats心跳总数 396 beats = 0; 397 processing.set(false); 398 return; 399 } 400 401 if(beatsIndex == beatsArraySize) { 402 beatsIndex = 0; 403 } 404 beatsArray[beatsIndex] = dpm; 405 beatsIndex++; 406 407 int beatsArrayAvg = 0; 408 int beatsArrayCnt = 0; 409 for (int i = 0; i < beatsArray.length; i++) { 410 if (beatsArray[i] > 0) { 411 beatsArrayAvg += beatsArray[i]; 412 beatsArrayCnt++; 413 } 414 } 415 int beatsAvg = (beatsArrayAvg / beatsArrayCnt); 416 mTV_Heart_Rate.setText("您的心率是"+String.valueOf(beatsAvg) + 417 " 值:" + String.valueOf(beatsArray.length) + 418 " " + String.valueOf(beatsIndex) + 419 " " + String.valueOf(beatsArrayAvg) + 420 " " + String.valueOf(beatsArrayCnt)); 421 //获取系统时间(ms) 422 startTime = System.currentTimeMillis(); 423 beats = 0; 424 } 425 processing.set(false); 426 } 427 }; 428 429 /** 430 * 预览回调接口 431 */ 432 private static SurfaceHolder.Callback surfaceCallback = new SurfaceHolder.Callback() { 433 //创建时调用 434 @Override 435 public void surfaceCreated(SurfaceHolder holder) { 436 try { 437 camera.setPreviewDisplay(previewHolder); 438 camera.setPreviewCallback(previewCallback); 439 } catch (Throwable t) { 440 Log.e("PreviewDemo","Exception in setPreviewDisplay()", t); 441 } 442 } 443 444 //当预览改变的时候回调此方法 445 @Override 446 public void surfaceChanged(SurfaceHolder holder, int format, int width,int height) { 447 Camera.Parameters parameters = camera.getParameters(); 448 parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH); 449 Camera.Size size = getSmallestPreviewSize(width, height, parameters); 450 if (size != null) { 451 parameters.setPreviewSize(size.width, size.height); 452 } 453 camera.setParameters(parameters); 454 camera.startPreview(); 455 } 456 457 //销毁的时候调用 458 @Override 459 public void surfaceDestroyed(SurfaceHolder holder) { 460 461 } 462 }; 463 464 /** 465 * 获取相机最小的预览尺寸 466 */ 467 private static Camera.Size getSmallestPreviewSize(int width, int height, Camera.Parameters parameters) { 468 Camera.Size result = null; 469 for (Camera.Size size : parameters.getSupportedPreviewSizes()) { 470 if (size.width <= width && size.height <= height) { 471 if (result == null) { 472 result = size; 473 } 474 else { 475 int resultArea = result.width * result.height; 476 int newArea = size.width * size.height; 477 if (newArea < resultArea) { 478 result = size; 479 } 480 } 481 } 482 } 483 return result; 484 } 485 }