Android通过摄像头检测心率

话不多说,先看效果

Android通过摄像头测量心率

借鉴文章如下
Android通过摄像头计算心率、心率变异性

该文章的核心功能点已经很全了,为了方便使用,我这边整理成了工具类可直接使用

该功能全网文章还是比较少的,还是要感谢下借鉴文章作者

该文章用法

  1. 动态申请权限少不了,不仅仅是摄像头权限还有麦克风权限,如果不自己申请,CameraView 内部也会自己判断的
  1. 首先进行摄像头监听
  2. 此时打开摄像头

平时的检测的时候都要打开闪光灯,我一开始的思路是单独去打开闪光灯,会报出摄像头被占用的错误,所以只能从使用的摄像头下手,因为用的是 CameraView 经源码勘测,发现打开闪光灯代码如下

  1. 在检测过程中会有手指挪开的问题,这里加了个判断,移开五次以上视为检测失效
  1. 在界面离开的时候别忘记销毁相机和还原数据

上述就是部分代码功能说明

以下是demo 全部代码

  1. 添加依赖
java 复制代码
   implementation 'com.otaliastudios:cameraview:2.7.2'

2.清单文件中添加权限

java 复制代码
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.FLASHLIGHT" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-feature android:name="android.hardware.camera" />
    <uses-feature android:name="android.hardware.autofocus" />
  1. Activity代码
java 复制代码
class MainActivity : AppCompatActivity() {

    private lateinit var vwCamera: CameraView
    private lateinit var tvTip1: TextView
    private lateinit var tvBpm: TextView
    private lateinit var vwLine: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        vwCamera = findViewById<CameraView>(R.id.vwCamera)
        tvTip1 = findViewById<TextView>(R.id.tvTip1)
        tvBpm = findViewById<TextView>(R.id.tvBpm)
        vwLine = findViewById<TextView>(R.id.vwLine)

        if (checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
            vwCamera.setLifecycleOwner(this)
            //监听摄像头状态
            vwCamera.addCameraListener(object : CameraListener() {
                override fun onCameraOpened(options: CameraOptions) {
                    super.onCameraOpened(options)
                    //摄像头开启之后,打开闪光灯
                    vwCamera.flash = Flash.TORCH
                }
            })
            vwCamera.addFrameProcessor {
                //每一帧的回调,在这里检查用户是否放上手指,并做计时然后计算心率
                HeartRateUtils.handleFrameCamera(vwCamera,it,this,object :HeartRateUtils.HeartRateInterFace{
                    override fun callTipsBack(tips: String) {
                        //这里是摄像头是否检测到手指的回调
                        tvTip1.text = tips
                    }

                    override fun callRateBack(rate: String) {
                        //这里是心率数据回调
                        tvBpm.text = rate
                    }

                    override fun callEndRateBack(end: String) {
                        //这里是检测结束的回调  end 是最终心率
                        tvBpm.text =  "检测结束: ${end}"
                    }

                    override fun callAngleBack(angle: String) {
                        //这里是手机抖动回调- 看需求是否需要
                        vwLine.text = "手机抖动:${angle}"
                    }

                })
            }

            HeartRateUtils.startScanView(this)
        } else {
            //否则去请求相机权限
            ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA), 100);
        }

    }

    override fun onDestroy() {
        super.onDestroy()
        HeartRateUtils.closeScanView()
    }

}
  1. 布局文件代码
java 复制代码
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    tools:context=".MainActivity">

    <com.otaliastudios.cameraview.CameraView
        android:id="@+id/vwCamera"
        android:layout_width="80dp"
        android:layout_height="80dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

    <TextView
        android:id="@+id/tvTip1"
        android:layout_marginTop="10dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>


    <TextView
        android:id="@+id/tvBpm"
        android:layout_marginTop="10dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <TextView
        android:id="@+id/vwLine"
        android:layout_marginTop="10dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>


</androidx.appcompat.widget.LinearLayoutCompat>
  1. 工具类代码
java 复制代码
/**
 * 心跳检测数据类*/
object HeartRateUtils {

    private var vwCamera: CameraView? = null

    private val processing = AtomicBoolean(false)
    private var beatBeanTimeStart = 0L
    private val averageArraySize = 4
    private val averageArray = IntArray(averageArraySize)
    //开始时间
    private var startTime: Long = 0
    //设置默认类型
    private var currentType = TYPE.GREEN
    private var averageIndex = 0
    //心跳脉冲
    private var beats = 0f
    private var allBeats = 0
    private var flag = 1.0
    //心跳次数无效次数,默认大于5次就无效了
    private var noGet = 0

    private var beatBeanTimeList = arrayListOf<Long>()
    private val vwLinePos = mutableListOf<Float>()
    private val vwLinePosValue = mutableListOf(20f, 0f, -20f, 10f, -10f, 0f)

    //心跳数组
    private val beatsArray = arrayListOf<Int>()

    /**
     * 类型枚举
     */
    enum class TYPE {
        GREEN, RED
    }

    interface HeartRateInterFace{
        fun callTipsBack(
            //文本说明
            tips: String,
        )
        fun callRateBack(
            //测量中心率数据
            rate: String,
        )
        fun callEndRateBack(
            //最终结果心率
            end: String,
        )
        fun callAngleBack(
            //手机平稳度
            angle: String,
        )
    }

    fun startScanView(activity: AppCompatActivity) {
        vwCamera?.open()
    }

    private fun stopScanView() {
        vwCamera?.stopVideo()
    }


    fun closeScanView(){
        noGet = 0
        allBeats = 0
        beats = 0f
        beatsArray.clear()
        vwCamera?.close()
        vwCamera = null
    }

    /**
     * 处理每一帧的图像,已经在子线程中处理
     */
    fun handleFrameCamera(camera: CameraView,frame: Frame,activity: AppCompatActivity,callBack: HeartRateInterFace) {
        vwCamera = camera
        val size: Size = frame.size
        if (frame.dataClass === ByteArray::class.java) {
            val data: ByteArray = frame.getData()
            //processing是true直接退出这一步流程,初始执行是false,第一步在这里赋值为true了,如果还没执行到后面的流程下一帧就来了的话直接舍弃掉这一帧
            if (!processing.compareAndSet(false, true)) return
            val width: Int = size.width
            val height: Int = size.height
            //图像处理
            val imgAvg: Int = decodeYUV420SPtoRedAvg(data.clone(), height, width)
            //imgAvg小于200表示手指没有覆盖到摄像头,这时候终端整个流程还是说保存流程在一定时间内重启的话就继续
            if (imgAvg < 200) {
                activity.runOnUiThread {
                    stopScanView()
                    callBack.callTipsBack("将手指放在相机镜头和闪光灯上")
                }
            } else
                activity.runOnUiThread {
                    callBack.callTipsBack("正在检测,请保持手指位置")
                }
            //像素平均值imgAvg,日志
            //Log.i(TAG, "imgAvg=" + imgAvg);
            if (imgAvg == 0 || imgAvg == 255) {
                //红色像素的均值为0或者255时表示极端不正确的情况,直接退出,并重置标志位为false,下一帧图片再次执行赋值为true
                processing.set(false)
                beatBeanTimeStart = 0L//如果中途有手指移出等情况就清空上次保存的心跳时间,约等于舍弃掉这次不正常的记录
                return
            }
            //计算4次帧图的像素均值列表值的和与个数
            var averageArrayAvg = 0
            var averageArrayCnt = 0
            for (i in averageArray.indices) {
                if (averageArray[i] > 0) {
                    averageArrayAvg += averageArray[i]
                    averageArrayCnt++
                }
            }
            //计算整体全部帧图像素平均值
            val rollingAverage = if (averageArrayCnt > 0) averageArrayAvg / averageArrayCnt else 0
            if (rollingAverage == 0 && imgAvg > 200) {
                startTime = System.currentTimeMillis()
            }
            var newType: TYPE = currentType
            //如果当前帧像素平均值小于前4次帧像素平均值的话
            if (imgAvg in 201 until rollingAverage) {
                newType = TYPE.RED
                if (newType != currentType) {
                    beats++
                    allBeats++
                    flag = 0.0
                    //这里表示心跳了一下,保存与上一次心跳的时间间隔,后续要用作心率变异性计算
                    if (beatBeanTimeStart == 0L) {
                        //保存第一次心跳
                        beatBeanTimeStart = System.currentTimeMillis()
                    } else {
                        val nowTime = System.currentTimeMillis()
                        val rrTime = nowTime - beatBeanTimeStart
                        if (rrTime in 400..1400)
                            beatBeanTimeList.add(nowTime - beatBeanTimeStart)
                        beatBeanTimeStart = nowTime
                    }
                    vwLinePos.clear()
                    activity.runOnUiThread {
                        vwLinePos.add(20f)
                        callBack.callAngleBack(vwLinePos.last().toString())
                    }
                }
            } else {
                //心脏跳动控制六帧,六帧后恢复平静
                newType = TYPE.GREEN
                activity.runOnUiThread {
                    if (vwLinePos.size < 6 && allBeats > 0) {
                        vwLinePos.add(vwLinePosValue[vwLinePos.size])
                        callBack.callAngleBack(vwLinePos.last().toString())
                    } else {
                        vwLinePos.add(0f)
                        callBack.callAngleBack(vwLinePos.last().toString())
                    }
                }
            }

            //保存4次帧像素平均值,4次后重置
            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)
            val endTime = System.currentTimeMillis()
            val totalTimeInSecs: Float =
                (endTime - startTime) / 1000f//当前帧到达的时间减去摄像头初始的时间就等于第一帧的时间差
            if (totalTimeInSecs >= 2) {//2秒处理一次
                val bps: Float =
                    beats / totalTimeInSecs//脉冲次数表示当前间隔时间内的心跳次数,心跳次数/心跳持续时间  =  一秒内的心跳次数
                val dpm = (bps * 60.0).toInt()//1秒内的心率乘以60等于一分钟内的心率也就是计算出来的心率值
                Log.e("心率检测", "time:$totalTimeInSecs,2秒内的心跳次数:$beats, 每秒心跳次数:$bps, 心率:$dpm")
                if (dpm < 30 || dpm > 180 || imgAvg < 200) {//这里是心率不合规的情况,心率小于30或者心率大于180或者当前帧像素平均值小于200(手指未正确覆盖),初始化程序继续探查
                    //获取系统开始时间(ms)
                    startTime = System.currentTimeMillis()
                    beatBeanTimeStart = startTime
                    //beats心跳总数
                    beats = 0f
                    processing.set(false)
                    noGet++
                    Log.e("心率检测", "此次检测无效${noGet}")
                    if (noGet > 5){
                        callBack.callTipsBack("此次检测无效")
                        vwCamera?.close()
                    }
                    return
                }
                //存储正常心率进心率表,心率表只保存近三次数据,新的数据来会顶掉
                beatsArray.add(dpm)
                var beatsArrayAvg = 0
                var beatsArrayCnt = 0
                for (i in beatsArray) {
                    if (i > 0) {
                        beatsArrayAvg += i
                        beatsArrayCnt++
                    }
                }
                val beatsAvg = beatsArrayAvg / beatsArrayCnt
                activity.runOnUiThread {
                    if (beatsArray.size < 15) {
                        callBack.callRateBack(beatsAvg.toString())

                    }
                }
                //获取系统时间(ms)
                startTime = System.currentTimeMillis()
                beats = 0f
                //总共记录半分钟或者1分钟的心率变化,半分钟后给结果,就是记录15次,心率数组保存15次的数据后删除。如果想更准确的话就加大记录的心跳数值
                if (beatsArray.size == 15) {
                    vwCamera?.close()

                    //中断探测,得出结果去结果页
                    //心率等于一分钟心跳多少次,由于我们两秒钟记录一次所以这里的allBeats*2就应该是一分钟的心跳次数
                    val bpm = allBeats * 2//心率
                    Log.e("心率检测", "结束--${bpm}")
                    activity.runOnUiThread {
                        callBack.callEndRateBack(bpm.toString())
                    }
                }
            }
            processing.set(false)
        } else if (frame.dataClass === Image::class.java) {
            Log.e("心率检测", "camera2帧数据返回")
            val data: Image = frame.getData()
            // Process android.media.Image...
        }
    }



    /**
     * 内部调用的处理图片的方法
     */
    private fun decodeYUV420SPtoRedSum(yuv420sp: ByteArray?, width: Int, height: Int): Int {
        if (yuv420sp == null) return 0
        val frameSize = width * height
        var sum = 0
        var j = 0
        var yp = 0
        while (j < height) {
            var uvp = frameSize + (j shr 1) * width
            var u = 0
            var v = 0
            var i = 0
            while (i < width) {
                var y = (0xff and yuv420sp[yp].toInt()) - 16
                if (y < 0) y = 0
                if (i and 1 == 0) {
                    v = (0xff and yuv420sp[uvp++].toInt()) - 128
                    u = (0xff and yuv420sp[uvp++].toInt()) - 128
                }
                val y1192 = 1192 * y
                var r = y1192 + 1634 * v
                var g = y1192 - 833 * v - 400 * u
                var 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
                val pixel = (-0x1000000 or (r shl 6 and 0xff0000)
                        or (g shr 2 and 0xff00) or (b shr 10 and 0xff))
                val red = pixel shr 16 and 0xff
                sum += red
                i++
                yp++
            }
            j++
        }
        return sum
    }

    /**
     * 对外开放的图像处理方法
     */
    private fun decodeYUV420SPtoRedAvg(
        yuv420sp: ByteArray?, width: Int,
        height: Int
    ): Int {
        if (yuv420sp == null) return 0
        val frameSize = width * height
        val sum = decodeYUV420SPtoRedSum(yuv420sp, width, height)
        return sum / frameSize
    }

}
相关推荐
2401_8979078611 分钟前
10天学会flutter DAY2 玩转dart 类
android·flutter
m0_7482336438 分钟前
【PHP】部署和发布PHP网站到IIS服务器
android·服务器·php
Yeats_Liao2 小时前
Spring 定时任务:@Scheduled 注解四大参数解析
android·java·spring
雾里看山4 小时前
【MySQL】 库的操作
android·数据库·笔记·mysql
水瓶丫头站住12 小时前
安卓APP如何适配不同的手机分辨率
android·智能手机
xvch12 小时前
Kotlin 2.1.0 入门教程(五)
android·kotlin
xvch16 小时前
Kotlin 2.1.0 入门教程(七)
android·kotlin
望风的懒蜗牛16 小时前
编译Android平台使用的FFmpeg库
android
浩宇软件开发17 小时前
Android开发,待办事项提醒App的设计与实现(个人中心页)
android·android studio·android开发
ac-er888818 小时前
Yii框架中的多语言支持:如何实现国际化
android·开发语言·php