美文网首页
通过摄像头监测心率

通过摄像头监测心率

作者: iOS_tree | 来源:发表于2019-03-12 11:12 被阅读0次

    导言

    现在通过手机摄像头监测心跳的APP非常之多,原理是通过摄像头收集手指的光线变化影像进行判断。原理网上有许多解释,我们自己也可以进行实践。当我们自己把闪光灯打开,把手指轻轻放在摄像头上,也可以看到图像颜色的轻微周期性的变化,这种颜色周期性的变化即心脏的周期性跳动。本人根据网上的资料初略实现了心跳检测,写下来供大家参考,其中若有错误或者可以优化的地方请大家留言。

    原理

    采集摄像头的图片数据,转换成RGB数据,然后转化成HSV数据,取其中的H数据作为数据分析的根据。对H数据进行差分阈值法处理,然后使用基音检测算法求出心率。

    实现

    1、取到RGB数据

    我们在iOS手机上进行实现,安卓手机也一样,只是摄像头数据采集部分有所不同。摄像头数据采集部分在此不再叙述,可参考https://www.jianshu.com/p/f1e342945933。在iOS里面我们拿到的是CMSampleBufferRef数据,我们需要把CMSampleBufferRef转换成我们需要的RGB数据。代码如下:

        CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
        CVPixelBufferLockBaseAddress(imageBuffer, 0);
        uint8_t*buffer = (uint8_t *)CVPixelBufferGetBaseAddress(imageBuffer);
        size_t width = CVPixelBufferGetWidth(imageBuffer);
        size_t height = CVPixelBufferGetHeight(imageBuffer);
    

    此时buffer里面的数据即为RGBA数据,每个数据占据8位,一个字节,内存排列为:B(一个字节)G(一个字节)R(一个字节)A(一个字节)BGRABGRA……;我们依次读取BGR数据,不需要A(alpha,透明通道)。我们求出RGB的平均值,代码如下:

        NSInteger totalR = 0;
        NSInteger totalG = 0;
        NSInteger totalB = 0;
        
        for (int i = 0; i < height * width; i++) {
            int b = buffer[i * 4];
            int g = buffer[i * 4 + 1];
            int r = buffer[i * 4 + 2];
            totalR += r;
            totalG += g;
            totalB += b;
        }
        
        double averageR = totalR * 1.0 / (width * height);
        double averageG = totalG * 1.0 / (width * height);
        double averageB = totalB * 1.0 / (width * height);
    
    

    2、RGB转换为HSV数据

    转换公式如下:


    RGB转HSV(其中,M = max(R, G, B), m = min(R, G, B))

    转换为H的具体代码如下:

    + (double)getHFromR:(double)r g:(double)g b:(double)b {
        
        double max = MAX(r, MAX(g, b));
        double min = MIN(r, MIN(g, b));
        double off = max - min;
        
        if (max == min) {
            return 0;
        }
        if (max == r && g >= b) {
            return 60.0 * (g - b) / off;
        }
        if (max == r && g < b) {
            return 60.0 * (g - b) / off + 360;
        }
        if (max == g) {
            return 60.0 * (b - r) / off + 120;
        }else {
            return 60.0 * (r - g) / off + 240;
        }
    }
    

    3、使用差分阈值法对H数据进行处理

    差分阈值法可以放大数据的波动,更好的提取数据的特征值,计算如下,以后一个数据减去前一个数据的差值作为特征值。如{1.1,1.3,1.4,1},处理后为{0.2,0.1,-0.4}。代码如下:

    // 记录浮点变化的前一次的值
    static float lastValue = 0;
    // 用于判断是否是第一个浮点值
    static int   count = 0;
    //使用差分阈值法处理数据
    - (float)differenceThresholdArithmetic:(float)value {
        float low = 0;
        count++;
        lastValue = (count == 1) ? value : lastValue;
        low = (value - lastValue);
        lastValue = value;
        return low;
    }
    

    经过测试,阈值差分法计算出的正常值不会超过1,我们需要把超过1的异常数据进行过滤。

    4、使用基音检测算法求出心率

    算法原理为首先估算出大概的心率周期,然后在3个周期的数据里面寻找一个最低值,然后在该值的前1.5-0.5周期和该值的后0.5-1.5周期里面分别寻找到最低值,然后取两个较低值中的一个与最低值进行时间比较,计算心跳周期,然后计算出心率。如下图:


    基音检测算法示意图

    其中,左次低点和右次低点至少存在一个,若存在一个则计算左次低点或者右次低点与最低点的时间差值,若存在两个则选取更加接近最低点的一个值进行计算。保存特征值时需要同时保存值的提取时间,做周期计算使用。
    代码如下:

    - (void)analysisPointsWith:(NSDictionary *)point {
        
        [self.points addObject:point];
        //样本过少
        if (self.points.count <= self.T * 3){
            return;
        }
        int count = (int)self.points.count;
        
        int minIndex = 0;                   //最低峰值的位置 姑且算在中间位置
        int minLeftMinIndex = 0;          //最低峰值左面的最低峰值位置
        int minRightMinIndex = 0;          //最低峰值右面的最低峰值位置
        
        float minTroughValue = 0;     //最低峰值的浮点值
        float minLeftTroughValue = 0;     //最低峰值左面的最低峰值浮点值
        float minRightTroughValue = 0;     //最低峰值右面的最低峰值浮点值
        
        // 1.先确定数据中的最低峰值
        for (int i = 0; i < count; i++) {
            float trough = [[[self.points[i] allObjects] firstObject] floatValue];
            if (trough < minTroughValue) {
                minTroughValue = trough;
                minIndex = i;
            }
        }
        
        //2.求左边峰值,如果左边的周期大于0.5个周期,则求出左边的峰值
        if (minIndex > 0.5 * self.T) {
            int startLeftIndex = minIndex - 1.5 * self.T;
            if (startLeftIndex < 0) {
                startLeftIndex = 0;
            }
            for (int j = startLeftIndex; j < minIndex - 0.5 * self.T; j++) {
                float trough = [[[self.points[j] allObjects] firstObject] floatValue];
                if ((trough < minLeftTroughValue) && (minIndex - j) <= self.T) {
                    minLeftTroughValue = trough;
                    minLeftMinIndex = j;
                }
            }
        }
        
        //3.求右边峰值,如果右边的周期大于0.5个周期,则求出右边的峰值
        if (minIndex < count - 0.5 * self.T) {
            int endRightIndex = minIndex + 1.5 * self.T;
            if (endRightIndex > count) {
                endRightIndex = count;
            }
            for (int k = minIndex + 0.5 * self.T; k < endRightIndex; k++) {
                float trough = [[[self.points[k] allObjects] firstObject] floatValue];
                if ((trough < minRightTroughValue) && (k - minIndex <= self.T)) {
                    minRightTroughValue = trough;
                    minRightMinIndex = k;
                }
            }
        }
        
        // 3. 确定哪一个与最低峰值更接近 用最接近的一个最低峰值测出瞬时心率 60*1000两个峰值的时间差
        int min_index_rl = minLeftMinIndex;
        if (minLeftTroughValue > minRightTroughValue) {
            min_index_rl = minRightMinIndex;
        }
        
        NSDictionary *first_point = self.points[minIndex];
        NSDictionary *second_point = self.points[min_index_rl];
        double first_time = [[[first_point allKeys] firstObject] doubleValue];
        double second_time = [[[second_point allKeys] firstObject] doubleValue];
        int fre = (int)((60 * 1000) / (first_time - second_time));
        fre = abs(fre); //fre即为即时计算心率
        
        // 4.删除过去一个周期的数据
        for (int i = 0; i< self.T; i++) {
            [self.points removeObjectAtIndex:0];
        }
    }
    

    5、绘制心率曲线

    绘制曲线图时我们需要保存历史数据,我使用一个环形双向链表数据结构进行保存读取数据,使用时非常方便。数据结构体及初始化代码如下:

    //数据结构体
    struct ESCValueStruct {
        double value;
        struct ESCValueStruct *preStruct;
        struct ESCValueStruct *nextStruct;
    };
    //链表初始化代码如下:
            int dataCount = 1024 * 4;
            void *data = malloc(sizeof(ESCValueStruct) * dataCount);
            self.data = data;
            ESCValueStruct *firstPoint = data;
            firstPoint->value = 0;
            ESCValueStruct *lastPoint = firstPoint;
            
            for (int i = 1; i < dataCount; i++) {
                ESCValueStruct *currentValueStruct = (data + sizeof(ESCValueStruct) * i);
                currentValueStruct->value = 0;
                lastPoint->nextStruct = currentValueStruct;
                currentValueStruct->preStruct = lastPoint;
                lastPoint = currentValueStruct;
            }
            
            lastPoint->nextStruct = firstPoint;
            firstPoint->preStruct = lastPoint;
            
            self.currentValueStruct = data;
    
    

    在自定义的UIView中的- (void)drawRect:(CGRect)rect方法中使用UIBezierPath进行绘制。绘制代码如下:

    - (void)drawRect:(CGRect)rect {
        CGFloat height = rect.size.height;
        CGFloat width = rect.size.width;
        int step = 2;
        [[UIColor greenColor] setStroke];
        UIBezierPath *path = [UIBezierPath bezierPath];
        path.lineWidth = 1;
    
        CGFloat value = self.currentValueStruct->value;
        CGFloat y = height / 2 - value * height / 2;
        [path moveToPoint:CGPointMake(width, y)];
    
        ESCValueStruct *valueStruct = self.currentValueStruct->preStruct;
        
        for (int i = 1; i < width / step; i++) {
            CGFloat value = valueStruct->value;
            CGFloat y = height / 2 - value * height / 2;
            [path addLineToPoint:CGPointMake(width - i * step, y)];
            valueStruct = valueStruct->preStruct;
        }
        [path stroke];
    }
    

    最终展示如下:


    效果图

    demo地址:https://github.com/XMSECODE/ESCHeartRateDemo

    参考地址:
    https://blog.csdn.net/fishmai/article/details/73457457
    https://blog.csdn.net/qq_30513483/article/details/52604148
    算法可继续优化,需要者可参考:https://wenku.baidu.com/view/af7faf4eddccda38376baf8e.html

    相关文章

      网友评论

          本文标题:通过摄像头监测心率

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