美文网首页iOS开发iOSIOS开发资料库
心跳之旅—💗—iOS用手机摄像头检测心率(PPG)

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

作者: 胖绵羊 | 来源:发表于2016-07-28 20:56 被阅读10339次

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

本文中涉及到的要点主要有:

  • AVCapture
  • Core Graphics
  • Delegate & Block
  • RGB -> HSV
  • 带通滤波
  • 基音标注算法(TP-Psola)
  • 光电容积脉搏波描记法(PhotoPlethysmoGraphy, PPG)

在开始之前,我先为大家展示一下最后成品的效果:

心率检测的ViewController

上图展示的是心率检测过程中的主要界面。
在检测的过程中,应用能够实时捕捉心跳的波峰,计算相应的心率,并以Delegate或Block的形式回调,在界面上显示相应的动画和音效。


〇、剧情概览

好吧,😂其实上面的前情提要都是我瞎掰的,这个Demo是我来到公司的第一天接到的任务。刚接到任务的时候其实是有点懵逼的,原本以为刚入职两天可能都是要看看文档,或者拖拖控件,写写界面什么的,结果Xcode都还没装好,突然接到一个心率检测的任务,顿时压力就大起来了😨,赶紧拍拍屁股起来找资料。

心率检测的APP在我高三左右就有了,我清楚地记得当时,年少无知的我还误以为,大概又是哪个刁民闲着无聊恶搞的流氓应用,特地下载下来试了一下,没想到居然真的能测。。。


总有刁民想害朕

当时就震惊地打开了某度查了这类应用的原理。所以现在找起资料来还是比较有方向性的。

花了一天的时间找资料,发现在手机心率检测方面,网上相关的东西还是比较少。不过各种资料参考下来,基本的实现思路已经有了。

任务清单

  • 实现心率检测

一、整体思路

原理

首先说一说用手机摄像头实现心率检测所用到的原理。
我们知道,现在市面上有非常多具备心率检测功能的可穿戴设备,比如各种手环以及各种Watch,其实从本质上讲,我们这次要用到的原理跟这些可穿戴设备所用到的原理并无二致,它们都是基于光电容积脉搏波描记法(PhotoPlethysmoGraphy, PPG)

iWatch的心率传感器发出的绿光

PPG是追踪可见光(通常为绿光)在人体组织中的反射。它具备一个可见光光源来照射皮肤,再使用光电传感器采集被皮肤反射回来的光线。PPG有两种模式,透射式和反射式,像一般的手环手表这样,光源和传感器在同一侧的,就是反射式;而医院中常见的夹在指尖上的通常是透射式的,即光源和传感器在不同侧。
皮肤本身对光线的反射能力是相对稳定的,但是心脏泵血使得血管容积周期性地变化,导致反射光也呈现出周期性的波动值,特别是在指尖这种毛细血管非常丰富的部位,这种周期性的波动很容易被观察到。

使用iPhone的系统相机就可以轻易地用肉眼观察到这种波动——在录像中打开闪光灯,然后用手指轻轻覆盖住摄像头,就能观察到满屏的红色图像会随着心跳产生一阵一阵的明暗变化,如下图(请忽略满屏的摩尔纹)。

直接用肉眼就能观察到相机图像的明暗变化

至于,为什么可穿戴设备上用的光源大多数都是绿光,我们用手机闪光灯的白光会不会有问题。这主要是因为绿光在心率检测中产生的信噪比比较大,有利于心率的检测,用白光也是完全没问题的。详情可以移步知乎:各种智能穿戴的心率检测功能 。我在这里就不细说了。

我的思路

我们已经知道我们需要用闪光灯和摄像头来充当PPG的光源和传感器,那么下面就来分析一下后续整体的方案。下面是我搜集完数据之后大致画出的一个流程图。

整体思路
  1. 首先我们需要采集相机的数据,这一步可以使用AVCapture;
  2. 然后按照某种算法,对每一帧图像计算出一个相应的特征值并保存到数组中,算法可以考虑取红色分量或者转换为HSV再计算;
  3. 在得到一定量的数据后,我们对这个时间段内的数据进行预处理,譬如进行滤波,过滤掉一些噪声,可以参考一篇博客:巴特沃斯滤波器
  4. 接下来,就可以进行心率计算,这一步可能涉及到一些数字信号处理的内容,例如波峰检测,信号频率计算,可以使用Accelerate.Framework的vDSP处理框架,Accelerate框架的用法可以参考:StackOverFlow的一个回答(最终我并没有使用,原因后面会提到);
  5. 最终就可以得到心率。

二、初步实现

有了大概的方案之后,我决定着手进行实现了。

1)视频流采集

我们前面已经提到,我们要用AVCapture进行视频流的采集。在使用AVCapture的时候,需要先建立AVCaptureSession,相当于是一个传输流,用来连接数据的输入输出,然后分别建立输入和输出的连接。因此,为了更加直观,我先做了一个类似于相机的Demo,把AVCapture采集到的相机图像直接传输到一个Layer上。

  1. 创建AVCaptureSession
    AVCaptureSession的配置过程类似于一次数据库事务的提交。开始配置前必须调用[_session beginConfiguration];来开始配置;完成所有的配置工作后,再调用[_session commitConfiguration];来提交此次配置。
    因此,整个配置过程大致是这样的:

     /** 建立输入输出流 */
     _session = [AVCaptureSession new];
     /** 开始配置AVCaptureSession */
     [_session beginConfiguration];
     /*
      * 配置session
      * (建立输入输出流)
      * ...
      */
     /** 提交配置,建立流 */
     [_session commitConfiguration];
     /** 开始传输数据流 */
     [_session startRunning];       
    
  2. 建立输入流From Camera
    要从相机建立输入流,就得先获取到照相机设备,并且对它进行相应的配置。这里对照相机的配置最关键的是要打开闪光灯常亮。此外,再设置一下白平衡、对焦等参数的锁定,来保证后续的检测过程中,不会因为相机的自动调整而导致特征值不稳定。

     /** 获取照相机设备并进行配置 */
     AVCaptureDevice *device = [self getCameraDeviceWithPosition:AVCaptureDevicePositionBack];
     if ([device isTorchModeSupported:AVCaptureTorchModeOn]) {
         NSError *error = nil;
         /** 锁定设备以配置参数 */
         [device lockForConfiguration:&error];
         if (error) {
             return;
         }
         [device setTorchMode:AVCaptureTorchModeOn];
         [device unlockForConfiguration];//解锁
     }
    

需要注意的是,照相机Device的配置过程中,需要事先锁定它,锁定成功后才能进行配置。并且,在配置闪光灯等参数前,必须事先判断当前设备是否支持相应的闪光灯模式或其他功能,确保当前设备支持才能够进行设置。
此外,对于相机的配置,还有一点非常重要:记得调低闪光灯亮度!!

长期打开闪光灯会使得电池发热,这对电池是一种伤害。在我调试的过程中,曾经无数次调着调着忘了闪光灯还没关,最后整只手机发热到烫手的程度才发现,直接进化成小米~ 所以,尽量将闪光灯的亮度降低,经过我的测试,即使闪关灯亮度开到最小也能够测得清晰的心率。

接下来就是利用配置好的device创建输入流:

    /** 建立输入流 */
    NSError *error = nil;
    AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:device
                                                                              error:&error];
    if (error) {
        NSLog(@"DeviceInput error:%@", error.localizedDescription);
        return;
    }
  1. 建立输出流To AVCaptureVideoDataOutput
    建立输出流需要用到AVCaptureVideoDataOutput类。我们需要创建一个AVCaptureVideoDataOutput类并设置它的像素输出格式为32位的BGRA格式,这似乎是iPhone相机的原始格式(经@熊皮皮提出,除了这种格式,还有两种YUV的格式)。后续我们读取图像Buffer中的像素时,也是按照这个顺序(BGRA)去读取像素点的数据。设置中需要用一个NSDictionary来作为参数。
    我们还要设置AVCaptureVideoDataOutput的代理,并创建一个新的线程(FIFO)来给输出流运行。

     /** 建立输出流 */
     AVCaptureVideoDataOutput *videoDataOutput = [AVCaptureVideoDataOutput new];
     NSNumber *BGRA32PixelFormat = [NSNumber numberWithInt:kCVPixelFormatType_32BGRA];
     NSDictionary *rgbOutputSetting;
     rgbOutputSetting = [NSDictionary dictionaryWithObject:BGRA32PixelFormat
                                                    forKey:(id)kCVPixelBufferPixelFormatTypeKey];
     [videoDataOutput setVideoSettings:rgbOutputSetting];    // 设置像素输出格式
     [videoDataOutput setAlwaysDiscardsLateVideoFrames:YES]; // 抛弃延迟的帧
     dispatch_queue_t videoDataOutputQueue = dispatch_queue_create("VideoDataOutputQueue", DISPATCH_QUEUE_SERIAL);
     [videoDataOutput setSampleBufferDelegate:self queue:videoDataOutputQueue];
    
  2. 连接到AVCaptureSession
    建立完输入输出流,就要将它们和AVCaptureSession连接起来啦!
    这里需要注意的是,必须先判断是否能够添加,再进行添加操作,如下所示。

     if ([_session canAddInput:deviceInput])
         [_session addInput:deviceInput];
     if ([_session canAddOutput:videoDataOutput])
         [_session addOutput:videoDataOutput];
    
  3. 实现代理协议的方法,获取视频帧
    上面的步骤中,我们将self设为AVCaptureVideoDataOutput的delegate,那么现在我们就要在self中实现AVCaptureVideoDataOutputSampleBufferDelegate的方法xxx didOutputSampleBuffer xxx,这样在视频帧到达的时候我们就能够在这个方法中获取到它。

     #pragma mark - AVCaptureVideoDataOutputSampleBufferDelegate & Algorithm
     - (void)captureOutput:(AVCaptureOutput *)captureOutput
         didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
         fromConnection:(AVCaptureConnection *)connection {
         /** 读取图像Buffer */
         CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
         //
         // 我们可以在这里
         // 计算这一帧的
         // 特征值。。。
         //
         /** 转成位图以便绘制到Layer上 */
         CGImageRef quartzImage = CGBitmapContextCreateImage(context);
         /** 绘图到Layer上 */
         id renderedImage = CFBridgingRelease(quartzImage);
         dispatch_async(dispatch_get_main_queue(), ^(void) {
             [CATransaction setDisableActions:YES];
             [CATransaction begin];
             _imageLayer.contents = renderedImage;
             [CATransaction commit];
         });
     }
    

做到这里,我们已经获得了一个类似于相机的Demo,在屏幕上可以输出摄像头采集的画面了,接下来,我们就要在这个代理方法中对每一帧图像进行特征值的计算。

2)采样(计算特征值)

采样过程中,最关键的就是如何将一幅图像转换为一个对应的特征值。
我先将所有像素点转换为一个像素点(RGB):

累加合成一个像素点

转换成一个像素点之后,我们只剩下RGB三个数值,事情就变简单得多。在设计采样的算法的过程中,我进行了许多种尝试。
我先试着简单地使用R、G、B分量中的其中一个直接作为信号输入,结果都不理想。

- HSV色彩空间
想到之前图形学的课上有介绍过HSV色彩空间,是将颜色表示为色相、饱和度、明度(Hue, Saturation, Value)三个数值。

HSV色彩空间**[5]**
我想,既然肉眼都能观察到图像颜色的变化,而RGB又没有明显的反映,那HSV的三个维度中应该有某个维度是能够反映出它的变化的。我便试着转换为HSV,结果发现色相H随脉搏的变化很明显!于是,我就先确定用H值来作为特征值。

我简单地用Core Graphics直接在图像的Layer上画出H数值的折线:

色相H随脉搏的变化

3)心率计算

为了使得曲线更加直观,我对特征值稍做处理,又改变了一下横坐标的比例,得到如下截图。现在心率信号稳定以后,波峰已经比较明显了,我们开始进行心率的计算。

缩放后,稳定的时候的心率信号

最初,我想到的是利用快速傅里叶变换(FFT)对信号数组进行处理。FFT可以将时域的信号转换成频域的信号,也就得到了一段信号在各个频率上的分布,这样,我们就能通过判断占比最大的频率,就差不多能确定心率了。
但是可能由于我缺乏信号处理的相关知识,经过将近两天的研究,我还是看不懂跟高数课本一样的文档。。。
于是我决定先用暴力的方法算出心率,等能用的Demo出来之后,看看效果如何,再考虑研究算法的优化。

通过上面的曲线,我们可以看出,在信号稳定的时候,波峰还是比较清晰的。因此我想,我可以设置一个阈值,进行波峰的检测,只要信号超过阈值,就判定该帧处于一个波峰。然后再设置一个状态机,完成波峰波谷之间的状态转换,就能检测出波峰了。
因为从AVCapture得到的图像帧数为30帧,也就是说,每一帧代表1/30s的时间。那么我只需要数一数从第一个波峰到最后一个波峰之间,经过了多少帧,检测到了多少波峰,那么,就能算出每个波峰间的周期,也就能算出心率了。

这个想法非常简单,但是存在一个问题,那就是,阈值的设置。波峰的凸起程度并不是恒定的,有时明显,有时微弱。因此,一个固定的阈值肯定不能满足实际检测的需求。于是我想到我们可以根据心跳曲线波动的上下范围,来实时确定一个合适的阈值。我做了如下修改:

每次进行心率计算的时候,先找出整个数组的极大和极小值,确定数据上下波动的范围。
然后,根据这个范围的一个百分比,来确定阈值。

也就是说,一个特征值只有超过了整组数据的百分之多少,它才会被判定为波峰。
根据这个方法,我每隔一段时间对数据进行一遍检测,在Demo中实现了心率的计算,又对界面进行了简单的实现,大致的效果如下。

初步实现的心率检测Demo

使用的过程中还存在一定程度的误检率,不过总算是实现了心率检测~ 🎉🎉🎉


三、性能优化

在我粗略实现了心率检测的功能后,Leader提出了对性能进行优化的要求,顺便向我普及了一波Instruments的用法(以前我一直没有用过🙊)。

任务清单

  • 性能优化
  • 封装组件(delegate或block的形式);
  • 提供两种默认动画;

我用Instrument分析了心率检测过程中的CPU占用,发现占用率很高,维持在50%~60%左右。不过这在我的预料中,因为我的算法确实很暴力😂——每帧的图像是1920x1080尺寸的,在1/30秒内,要对这200多万个像素点进行遍历计算,还要转换成位图显示在layer上,隔一段时间还要计算一次心率。。。

我分析了CPU占用比较多的部分,归纳了几个可以考虑优化的方向

  • 降低采样范围
  • 降低采样率
  • 取消AV输出
  • 降低分辨率
  • 改进算法,去除冗余计算
  1. 降低采样范围
    现在的采样算法是对所有的像素点进行一次采样,我想着是否能够缩小采样的范围,例如只对中间某块区域采样,但试验后我发现,只对某块区域采样会使得检测到的波峰变得模糊,说明个别区域的采样并不具有代表性。
    接着我又想到了一个新的办法。我发现图像中,临近像素点的颜色差异很小,那么我可以跳跃着采样,每隔几列、每隔几行采样一次,这样一方面可以减少工作量,一方面对采样的效果的影响也可以减少。
    跳跃着采样
    采样的方式就像上图展示的一样,再设置一个常量用来调节每次跳跃的间距。这样一来,理论上,每次占用的时间就可以降低为原来的1/n^2,大大减少。经过几次尝试后,可以看到,采样算法所在的函数的CPU占用比例由原来的31%降低到了14%了。

在分析CPU占用时,我发现在循环中对RGB分别累加时,第一个R的运算占用100倍以上的时间。开始时以为可能是Red分量数值较大,计算难度大,Leader建议我使用位运算,但是我改成位运算后,瓶颈依旧存在,弄得我十分困惑。后来我试着把RGB的计算顺序换一下,结果发现,瓶颈和R无关,不论RGB,只要谁在第一位,谁就会成为瓶颈。后来我想到,这应该是CPU和内存之间的数据传输造成的瓶颈,因为像素点都存在一块很大的内存块里,在取第一个数据的时候可能速度比较慢,然后后面取临近数据的时候可能就有Cache了,所以速度回提高两个数量级。

  1. 降低采样率
    降低采样率就是将视频的帧数降低,我记得,不知道是香农还是谁,有一个定理,大概的意思就是说,采样率只要达到频率的两倍以上,就能检测出信号的频率。
    coderMoe童鞋指出,此处正式名称应为“耐奎斯特采样定理”~香农是参与者之一)
    人的心跳上限一般是160/分钟,也就是不到3Hz,那理论上,我们的采样率只要达到6帧/秒,就能够计算出频率。
    不过,由于我之前使用的算法还不是特别稳定,所以,当时我没有对采样率进行改变。
  • 取消AV输出
    之前我为了方便看效果,将采集到的视频图像输出到了界面上的一层Layer上,其实这个画面完全没必要显示出来。因此我去除了这部分的功能,这样一来,整体的CPU占用就降低到了33%以下。

  • 降低分辨率
    目前我们采集视频的大小是1920x1080,其实我们并不需要分辨率这么高。降低分辨率一方面可以减少需要计算的像素点,另一方面可以减少IO的时间。
    在我将分辨率降低到640x480:

      if ([_session canSetSessionPreset:AVCaptureSessionPreset640x480]) {
          /** 降低图像采集的分辨率 */
          [_session setSessionPreset:AVCaptureSessionPreset640x480];
      }
    

结果非常惊人,整体的CPU占用率直接降低到了5%左右!

  • 改进算法,去除冗余计算
    最后,我对算法中一些冗余的计算进行了优化,不过,由于CPU占用已经降低到了5%左右,真正的瓶颈已经消除,所以这里的改进并没有很明显的变化。

四、封装

此前,我们已经完成了一个大致可用的心率监测Demo,但在此之前,我着重考虑的都是如何尽快实现心率检测的功能,对整体的结构和对象的封装都没有太多的考虑,简直把OC的面向对象用成了面向过程。
那么我们接下来的一个重要任务,就是对我们的心率检测进行封装,使它成为一个可复用的组件。

任务清单

  • 封装组件并提供合理接口(delegate或block的形式);
  • 提供两种默认动画;

封装ViewController

最开始的时候,我想到的是对ViewController进行封装,这样别人有需要心率检测的时候,就可以弹出一个心率监测的ViewController,上面带有一些检测过程中的动画效果,检测完成后自动dismiss,并且返回检测到的心率。
我在protocol中声明了三个接口:

/**
 * 心率检测ViewController的代理协议
 */
@protocol MTHeartBeatsCaptureViewControllerDelegate <NSObject>
@optional
- (void)heartBeatsCaptureViewController:(MTHeartBeatsCaptureViewController *)captureVC
              didFinishCaptureHeartRate:(int)rate;
- (void)heartBeatsCaptureViewControllerDidCancel:(MTHeartBeatsCaptureViewController *)captureVC;
- (void)heartBeatsCaptureViewController:(MTHeartBeatsCaptureViewController *)captureVC
                       DidFailWithError:(NSError *)error;
@end

我将三个方法都设为了optional的,因为我还在ViewController中设置了三个相应的Block供外部使用,分别对应三个方法。

@property (nonatomic, copy)void(^didFinishCaptureHeartRateHandle)(int rate);
@property (nonatomic, copy)void(^didCancelCaptureHeartRateHandle)();
@property (nonatomic, copy)void(^didFailCaptureHeartRateHandle)(NSError *error);

封装心率检测类

对ViewController进行封装之后,我们可以看到,还是比较不合理的。这意味着别人只能使用我们封装起来的界面进行心率检测,如果使用组件的人有更好的交互方案,或者有特殊的逻辑需求,那他使用起来就会很不方便。因此,我们很有必要进行更深层次的封装
接下来,我将会剥离出心率检测的类,进行封装。

首先,我一点点剥离出心率检测的关键代码,放进新的MTHeartBeatsCapture类中。剥离的差不多之后,就发现满屏的代码都是红色的Error😲,花了一个下午,才把项目恢复到能运行的状态。

我在心率检测类中设置了两个方法:启动和停止。使用起来很方便。

/** 开始检测心率 */
- (NSError *)start;
/** 停止检测心率 */
- (void)stop;

然后,我重新设计了一个心率检测器的回调接口,依旧是delegate和block并存的。新的接口如下:

/**
 * 心率检测器的代理协议;
 * 可以选择Delegate或者block来获得通知,
 * 因此protocol中所有方法均为可选方法
 */
@protocol MTHeartBeatsCaptureDelegate <NSObject>
@optional
/** 检测到一次波峰(跳动),可通过返回值选择是否停止检测 */
- (BOOL)heartBeatsCapture:(MTHeartBeatsCapture *)capture heartBeatingWithRate:(int)rate;
/** 失去稳定信号 */
- (void)heartBeatsCaptureDidLost:(MTHeartBeatsCapture *)capture;
/** 得到新的特征值(30帧/秒) */
- (void)heartBeatsCaptureDataDidUpdata:(MTHeartBeatsCapture *)capture
@end

我在新的接口中加入了heartBeatsCaptureDidLost:,方便在特征值波动剧烈的时候进行回调,这样外部就能提醒用户姿势不对。而第三个方法,则是为了之后外部的动画view能够做出类似于心电图一样的动画效果,而对外传出数据。
我还移除了检测成功的回调didFinishCaptureHeartRate:,换成了heartBeatingWithRate:,把成功时机的判断交给了外部,当外部的开发人员认为检测的心率足够稳定了,就可以返回YES来停止检测。
此外,我还移除了遇到错误的回调DidFailWithError:,因为我发现,几乎所有可能遇到的错误,都是发生在开始前的准备阶段,因此,我改成了在start方法中返回错误信息,并且枚举出错误类型作为code,封装成NSError

typedef NS_OPTIONS(NSInteger, CaptureError) {
    CaptureErrorNoError             = 0,        /**< 没有错误 */
    CaptureErrorNoAuthorization     = 1 << 0,   /**< 没有照相机权限 */
    CaptureErrorNoCamera            = 1 << 1,   /**< 不支持照相机设备,很可能处于模拟器上 */
    CaptureErrorCameraConnectFailed = 1 << 2,   /**< 相机出错,无法连接到照相机 */
    CaptureErrorCameraConfigFailed  = 1 << 3,   /**< 照相机配置失败,照相机可能被其他程序锁定 */
    CaptureErrorTimeOut             = 1 << 4,   /**< 检测超时,此时应提醒用户正确放置手指 */
    CaptureErrorSetupSessionFailed  = 1 << 5,   /**< 视频数据流建立失败 */
};

主要的工作完成后,Leader给我提了不少意见,主要还是封装上存在的一些问题,很多地方没有必要对外公开,应该尽可能地对外隐藏,接口也应该尽量地精简,没必要的功能要尽可能的去掉。特别是对外公开的一个特征值数组(NSMutableArray),对外应该不可变,这一点我一直没有考虑到。

封装动画&改进动画

心率检测类封装完成后,我又剥离出显示心跳波形的部分,封装成一个MTHeartBeatsWaveView,使用的时候只要将动画View赋给MTHeartBeatsCapture作为delegate,该view上就能获取到特征值数据并进行显示。

动画改进:在测试的过程中,我发现波形动画显示的波形不太理想,View的大小是初始化的时候就确定的,但是心跳波动的幅度变化是比较大的,有时候一马平川,堪比飞机场,有时候波澜壮阔,直接超出View的范围。
因此我对动画的显示做了一个改进:能够根据当前波形的范围,计算出合适的缩放比,对心跳曲线的Y坐标进行动态的缩放,使它的上下幅度适合当前的View。
这个改进大大提高了用户体验。


五、优化

我们可以看到,先前得到的曲线已经能较好地反映出心脏的搏动,但是现在进行心率的计算还是存在一定的误检率。上图中展示的清晰的心跳曲线,实际上是比较理想的时候,测试中会发现,采样得到的数据经常存在较大的噪声和扰动,导致心率计算中经常会有波峰的误判。因此,我在以下两方面做了优化,来提高心率检测的准确度。

1、在预处理环节进行滤波

得到的曲线有时含有比较多的噪声

分析一下心率曲线里的噪声,我们会发现,噪声中含有一些高频噪声,这部分噪声可能是手指的细微抖动造成的,也可能是相机产生的一些噪点。因此,我找到了一个简易的实时的带通滤波器,对之前我们采样获得到的H值进行处理,滤除了一部分高频和低频的噪声。

加入滤波器处理后的心率信号

在经过滤波器的处理之后,我们得到的曲线就更加平滑啦。

2、参考TP-Psola算法,排除伪波峰

经过滤波器的处理之后,我们会发现,在每个心跳周期中,总会有一个小波峰,因为它不是真正的波峰,因此我称它为“伪波峰”,这个伪波峰非常明显,有时也会干扰到我们心率的检测,被算法误判为心跳波峰,导致心率直接翻倍

这个伪波峰出现是因为,除了外部的噪声之外,心脏本身的跳动周期中也会出现许多的“杂波”。我们来看一次心跳的完整过程。

心电图波形产生过程的动画 **[1]**

上图是一次心跳周期中,心脏的状态变化以及对应产生的波段。可以看到,在心脏收缩前后,人体也会有电信号刺激心脏舒张,这在心电图上会表现出若干次的波动。而血压也会有相应的变化,我们检测到的数据的波动就是这样形成的。

基音标注

简单地说,这个算法会标注出可能的波峰,然后通过动态规划排除掉伪波峰,就能得到真正的波峰啦。我根据这个算法的思路,实现了一个简化版的伪波峰排除算法。经过改进后的心率检测,经测试准确度达到了和Apple watch差不多的程度。(自我感觉良好😂,求轻喷~~)

实时波峰检测

我还希望提供一个实时的心跳动画,因此我还实现了一个实时的波峰检测。这样每次检测到一个波峰之后,就可以立刻通知delegate或者block,在界面上做出动画。

心率检测的ViewController

歇-后-语

由于这一章节是了一阵子之后才写的,因此我把它叫做——歇后语

这个心率检测的项目前后一共做了三个礼拜左右,虽然第一个Demo用了三四天就完成,但是后续的封装和优化却用了两个星期的时间,嗯,感触颇深。。。

从最开始的incredible,到最后的好意思说堪比Apple Watch,真的是一个很有成就感的过程。虽然期间遇到了不少困难,甚至有那么一两次觉得自己真的无解了,但到最后总能熬过去,山重水复疑无路,柳暗花明又一村。真的忍不住要念诗了,感觉很充实,很开心。

在做这个项目的过程中,我也得到了许多人的帮助。部门里的各位前辈、同事,在看到我的提问之后,非常热情地向我提供意见和资料。希望这篇博客会对大家有所帮助。谢谢大家~


【更新于2016/8/10】

coderMoe童鞋指出,文中 [三、2.降低采样率] 提到的 “定理” 正式的名称为“奈奎斯特采样定理”。

感谢这段时间以来,大家的鼓励和支持,前阵子我写这篇文章的时候,是万万没有想到会得到这么多人的关注的,实在是受宠若惊。有很多人详细地阅读了这篇博客,并且提出了重要的意见,甚至还有几位客官打赏了我(但是简书取现要满100RMB才行,所以目前我还无法享用这笔增肥基金🙊哈哈),真的很感谢你们。

我当时写这篇博客也花了不少时间,只怪我语文没学好,在言辞表达上、逻辑结构上,没能做得更好,大家如果有什么意见建议、或者不同的见解,希望能不吝赐教~~大家的关注和交流会让我更有动力分享博客,要知道,写作对我这种工科生而言,真的是,“”体力活“”。😂

有想要进一步关注我的朋友,可以收藏一下我正在搭建的博客,域名正在备案中,不过博客系统是已经搭起来了,有兴趣的朋友请移步:punmy.cn😋

另外,关于许多朋友非常关心的开源的问题,这两天上班比较忙,但是我会在近期确定是否开源,届时会通过简书更新,感谢关注


【更新于2016/8/18】
感谢大家的厚爱,收到Leader的回复,这个项目暂时不开源,不好意思。

但是大家如果有什么问题,欢迎继续和我探讨!😊


【更新于2016/8/19】
我的域名审核通过啦,欢迎访问:punmy.cn😋

另外,有朋友指出,iPhone相机支持的原始数据格式有三种,一种是文中提到的BGRA,另两种似乎是YUV的格式,我对这方面不太了解,感谢提出,详情请看文档。

此外,有个别同学,不经大脑不经谷歌,就一味指责我文中的图片造假
说什么,文中提到的“系统相机就可以明显观察到明暗变化”的那张照片,根本不可能拍出清晰的指纹。。。
拜托,各位大爷,那是摩尔纹,谁告诉你是指纹了???excuse me??不明白请谷歌,动不动就骂人,我真是谢(qu)谢(ni)你(da)们(ye)了。

你们是人吗???

不过也因此收到了一些朋友的打赏😂,谢谢大家了~

再次声明,欢迎理性探讨,拒绝BB~


The End.


延伸阅读

外部引用

[0]: 写出“前情提要”的时候,脑子里蹦出的是:previously on marvel agents of shield😂

[1]: 引用自维基百科,由Kalumet - selbst erstellt = 自己的作品,<a href="http://creativecommons.org/licenses/by-sa/3.0/" title="Creative Commons Attribution-Share Alike 3.0">CC BY-SA 3.0</a>,https://commons.wikimedia.org/w/index.php?curid=438152

[2]: 引用自维基百科,由Derivative: Hazmat2Original: Hank van Helvete - 此档案源起于以下档案或由以下档案加以编辑而成: EKG Complex en.svg,<a href="http://creativecommons.org/licenses/by-sa/3.0" title="Creative Commons Attribution-Share Alike 3.0">CC BY-SA 3.0</a>,https://commons.wikimedia.org/w/index.php?curid=31447770

[5]:引用自维基百科,由(3ucky(3all - Uploaded to en:File:HSV cone.png first (see associated log) by (3ucky(3all; then transfered to Commons by Moongateclimber.,<a href="http://creativecommons.org/licenses/by-sa/3.0" title="Creative Commons Attribution-Share Alike 3.0">CC BY-SA 3.0</a>,https://commons.wikimedia.org/w/index.php?curid=943857

相关文章

网友评论

  • KG丿夏沫:准予发现我确实是个孩子,你们玩的我第一次听到,我要自己动手实践去了,学海无涯
  • e94729d83598:哇!!好棒啊!
  • zhangwenqiang:博主,你做的app叫啥名字?
  • Metaphors:看了你的文章我想请教你一个问题,心率计算你是采用基音周期估算的算法,那最大波峰的基音周期T0是如何计算得出的呢
  • 大萌哥哥:为啥还不开源啊,都一年多了,开源到github怎么也得几千颗星啊,要有开源精神
    saki6y:不开源也正常,不然付费sdk怎么来的。
    胖绵羊:@大萌哥哥 :joy: 过奖了,不好意思,这个项目开源的事暂时被搁置了。因为这个项目不属于我个人,所以我不好擅自开源哈,还望谅解。:cry:
  • 大萌哥哥:求demo 啊 大啊啊啊啊神
  • Ucself:真棒,请问App叫什么名字,我下载一个试一试?
    胖绵羊:@Ucself 不好意思噢,这是我实习的时候做的一个 Demo,本身并没有打算上线噢,目前也没有开源,不好意思
  • Ucself:哥们,请问能留下demo吗?
  • 长鲜:今天无聊的时候弄二维码的东西,手指放上去,想到这个,搜一搜就看到你的了。 开源了吗? 学习一下,发现中间的算法挺麻烦的
  • a7c6cec64788:大神 你的滤波器 是自己写的 还是找的三方啊
  • zhao1zhihui:git上有一个是不是你写的 能不能给个demo
  • a7c6cec64788:求助大神 如何检测用户有没有把手指放在摄像头上 或者说 放在摄像头上的是不是手指
    胖绵羊:@a7c6cec64788 这块我做得不是很好,主要思路是看有没有检测到稳定的波形。
  • a7c6cec64788:大神 怎么检测到是手指而不是别的按在摄像头上面
  • 厦门第一帅哥:已经过了那么久了,麻烦开源一下吧,就算不开源,编译成framework给我们调用也可以啊!!!
  • 厦门第一帅哥:你倒是能上个DEMO吗?
  • 起个名字真难啊2015:大牛,图片所有的BGA我拿到了,但我不知道如何将这些unsign char * (全部都是字符/375...)转换为一个像素点求解
    胖绵羊:@起个名字真难啊2015 字符?我当时是写了个简单的采样算法,其实最简单的就是全部累加,然后平均一下,但是这样性能比较低,所以最好采样部分像素点就好。
  • 7afbf1ca39a9:请问、检测血压血氧是什么做特征值计算?
    胖绵羊:@Z了个M 不是很懂你的意思~
  • 风听海水听石:只想说两个字“膜拜”。
  • 焘哥:有木有demo 大神。
  • 533ddd5e2e7c:学习了
  • 知行合一认知升级:看了几页AVFoundation 的api,最终还是在这里找到了!终于找到了实现代理协议的方法,获取视频帧。
    得到每一帧图,这样就可以得到图像句柄,做人脸识别了。感谢!!!!!!!!
    胖绵羊:@慢跑20 哈哈哈,有帮助就好
  • SuyChen:什么时候开源 啊
  • l富文本l:楼主,就想问一句手指轻轻的覆盖住摄像头,你怎么能拍出那样的照片?
    胖绵羊:@lcying “那样的照片” 你指的应该是屏幕上的水波状纹理吧,那个是 摩尔纹,。
  • Claudlit:感谢分享,对我帮助很大
  • 羞答答不肯把头抬:给个精简版的demo啊..
  • iiNico:写得很详细,很棒
    胖绵羊:@IT猫 谢谢
  • 有偶像包袱的程序狗:求速度开源,分享分享给我长长见识,学学知识。楼主棒棒哒,给我demo,我要给你生猴子。
    胖绵羊:@卢浩仑 害怕,楼主是直男
  • Resurgent:真惭愧 刚开始做开发什么都不会 先看下你提供的那些资料 这个好有意思
    胖绵羊:@Resurgent 哈哈,确实很有意思,而且做出来很有成就感
  • eb4d28077e08:已关注,坐等大神源码,自己看的一脸懵逼
  • Dee_Das:为了读懂这篇文章,研究coreImange,然后研究视频,然后打算写个播放器,等播放器写完,在研究视频硬编码,然后。。感觉这个坑真大。。。
    ee5f88c22738:@Dee_Das 那你真的是傻 被忽悠得团团转:stuck_out_tongue:
  • eeeff373c2cc:大哥,你写的这么好怎么不开源呢,独乐乐,不如众乐乐,你有自己的难处我也是能理解的!期待你的开源!我在问一句,你怎么画出的那个心率图的,就是说知道坐标了,怎么画曲线呢,这个可以给我个demo?
    胖绵羊:@活出简单 画曲线不难哦~用Quartz画就很方便,直接创建一个path,然后往path添加点。每个点以特征值为y坐标,同时x坐标每次移动一点。最后把path画到context上就可以。可以搜一下Quartz画折线
  • 鸟人扎墨客:怎么torch调节亮度??
    胖绵羊:@鸟人扎墨客 配置相机的时候,有个settorchlevel吧。
  • Joab_Jin:心率图绘制出来了,怎么计算心率值?
    胖绵羊:@Joab_Jin sorry,项目没有开源,我不能直接把实现给你。但是你可以先参考我最初用的,设置阈值的方法。那个比较简单,就算算有多少个超出阈值的波峰就好了。
    Joab_Jin:@胖绵羊 能把算出来的 画心率Low后,通过算法得到心率(这个算法的实现)告诉我吗
    胖绵羊:@Joab_Jin 要用到一些DSP的知识,可以考虑用基音标注的算法
  • 2898876cb8ca:简直是个活大神
    胖绵羊:@LovableMixer 谢谢
  • 47457f0e1071:能不能说一下,怎么把所有像素点,转化为一个像素点1006441417(QQ)
    胖绵羊:@mika368 文中有个公式的图片呀,你看一下。其实就是把所有像素点的RGB分别累加,然后分别除以 像素点的个数和255的乘积。
  • 雨三楼:牛逼
  • SYSYSY:根据你写的,,,,,我并不能写出来😂 希望开源啊
    胖绵羊:@SYSYSY 那个地方遇到问题?我也是花了三个礼拜才写出来。
  • 科研位的潇洒哥:楼主还生气了,哈哈哈
  • cppcoder:你好,怎么按手指有要求吗?我在iPhone6上,照相机视频模式,打开闪光灯,按好手指,看不出颜色有明暗变化
    胖绵羊:@cppcoder 不要按太用力,就覆盖住摄像头就好。有时候确实不一定能看到,大概试两三次应该能看到。
  • 乄_伤大雅:好牛逼
  • 尐情緒:求demo,大牛。。
    胖绵羊:@尐情緒 暂不开源:joy:谢谢
  • 693105fbc9cb:很好很强大!赞~
    之前我也试图做一个心率检测的东西,结果卡在信号处理上了,搁浅了。
    胖绵羊:@im_brucezz 哈哈,我也有很多都是临时学的
    693105fbc9cb:@胖绵羊 作为一个没学过相关知识的人,更为懵逼😂
    胖绵羊:@im_brucezz 信号处理确实挺棘手的
  • c18456007e32:写毕设时,做的就是信号分析。你说我怎么没想到这个呢 哈哈 写的不错。
    胖绵羊:@vook 哈哈,谢谢
  • whaike:涨姿势了 :+1: 希望有开源
    胖绵羊:@whaike 抱歉,目前还不能开源
    胖绵羊:@whaike 谢谢关注
  • Damonwong:如果这东西是一份源码分析应该是篇好文章。因为你说的这些 git 上源码都能找到,我都看到过。
    胖绵羊:@Damonwong 不好意思,现在还没有开源噢。你不是说github上面你都看过了?那你也不需要我的demo了吧。:relieved:
    Damonwong:@胖绵羊 show me the code
    胖绵羊:@Damonwong 哦?同学你想表达什么呢?
  • 117454808a0e:我是初学者,想看看大神的Demo
    胖绵羊:@TOTTI161718 不好意思哦,这项目暂时不开源
  • 117454808a0e:大神!!! 小生十分佩服
    胖绵羊:@TOTTI161718 过奖啦,我也在实习
  • Janice_love:好完整的从接需求到优化过程,谢谢您的分享。期待会有更多哦。 :heart:
    胖绵羊:@Janice_love 谢谢~我会继续努力哒~
  • samzhao:大赞!不明觉厉!毫不犹豫地小赏了:sunglasses:
    胖绵羊:@samzhao 谢谢赞助我的增肥之路~
  • 腌臜GIRL邋遢MAN:认真
    胖绵羊:@腌臜GIRL邋遢MAN 谢谢
  • 新建rebuild:不明觉厉

    胖绵羊:@梅有忘记 :joy: 有什么疑问可以提出来一起探讨哦~
  • 世界好复杂:完美的纪录。逻辑清晰
    胖绵羊:@蚊香蝌蚪 谢谢~
  • 小马哥亿天洋:支持开源:grin:
    胖绵羊:@小马哥亿天洋 :joy:
    小马哥亿天洋:@胖绵羊 嘿嘿 我可以等:stuck_out_tongue_closed_eyes:
    胖绵羊:@小马哥亿天洋 :grin: 不好意思,目前Leader还没有开源的准备 :disappointed_relieved:
  • 我想走走:好厉害,看的我一脸萌比
    胖绵羊:@峰峰爱码 :joy: 我写得有点乱~sorry
  • 云抱住阳光太阳没放弃发亮:很有意思
    胖绵羊:@云抱住阳光太阳没放弃发亮 谢谢~ :blush:
  • A_sura:作者知识面真广,敢问作者能透漏下在那个公司高就嘛.都有喵神的指导,真心膜拜!
    胖绵羊:@A_sura :grin:谢谢
    A_sura:@胖绵羊 好吧,不过确实很厉害
  • Da_Lan:不错
    summer_code:收藏了 楼主开源吗?求demo 596175302@qq.com
    胖绵羊:@DaLan 谢谢
  • ee5f88c22738:https://github.com/chroman/HeartBeats你是参考了这个项目做的?
    ksang:@胖绵羊 github有Runtastic Heart Rate的连接吗???
    ZPCoder:@胖绵羊 大神 求后面的滤波处理 和 计算心率的方法 不需要你们公司的源码 给点草稿代码就行 参考一下 毫无头绪啊 :pray: :pray:
    胖绵羊:@小鳄鱼www 图像采集方面我有参考过这个的实现方式,这个项目实现了图像采集,但没有进行心率的计算。有兴趣可以再参加一下耐克的Runtastic Heart Rate,那个实现得比较好
  • 贵叔勇闯天涯: 等着开源啊,坐等大牛的洪荒之力巨作
    胖绵羊:@UncleWang1992 :joy:醉了醉了,谢谢
  • 神殇小鬼:神了~~~
    感觉和那个sleep cycle的应用想法有一拼啊。。。
    厉害~
    胖绵羊:@神殇小鬼 过奖 :grin:
  • d67ddecd40fe:降低采样频率的那个,叫做耐奎斯特准则
    胖绵羊:@coderMoe 感谢指出!! :blush:
  • e0619b4a9d19:厉害 大牛。希望开源 让我这样的菜鸟长点见识
    胖绵羊:@一笑奈何1234 谢谢
  • 郭秀才:太厉害了
    胖绵羊:@郭秀才 谢谢~
  • 民哥:求demo ,大牛牛
    民哥:@胖绵羊 我也在自己做运动健康的APP ,可惜还没做完呢
    胖绵羊:@民哥 谢谢~正在等leader的意见,看是否开源 :grin:
  • 叼奶嘴打天下:楼主可有demo
    胖绵羊:@man_in_black 有的,不过目前还没开源,这几天如果开源了我会更新到简书噢~
  • tsf_筱筱:好牛逼!
    胖绵羊:@tsf_筱筱 谢谢:yum::yum::yum:
  • dongwenbo:牛逼!
    胖绵羊:@dongwenbo 谢谢:joy:
  • 谁动了我的梦:有demo吗
    胖绵羊:@我的大名叫小爱 Sorry, 公司没有开源,后续公司准备推进一波开源计划,我找时间再优化下 Demo,到时看能不能开源吧,哈哈。
    我的大名叫小爱:@胖绵羊 开源没 ?感觉就你这个靠谱啊。
    胖绵羊:@谁动了我的梦 谢谢关注,后面如果开源了,我会在博客中更新哦~
  • 沙琪玛dd:写的很细致 对我帮助很大!
    胖绵羊:@沙琪玛dd :smile:
  • 科研位的潇洒哥:好棒啊
    胖绵羊:@科研位的潇洒哥 谢谢~

本文标题:心跳之旅—💗—iOS用手机摄像头检测心率(PPG)

本文链接:https://www.haomeiwen.com/subject/hbyajttx.html