簡(jiǎn)書上的文章已經(jīng)不再維護(hù)存谎,有興趣閱讀其他文章挺尾,或一起交流的朋友返顺,請(qǐng)移步 我的博客:punmy.cn
原文
[前情提要] 光陰似箭禀苦,日月如梭,最近幾年遂鹊,支持心率檢測(cè)的設(shè)備愈發(fā)常見了振乏,大家都在各種測(cè)空氣測(cè)雪碧的,如火如荼秉扑,于是我也來湊一湊熱鬧慧邮。[0]
這段時(shí)間调限,我完成了一個(gè)基于iOS的心率檢測(cè)Demo,只要穩(wěn)定地用指尖按住手機(jī)攝像頭误澳,它就能采集你的心率數(shù)據(jù)耻矮。Demo完成后,我對(duì)心率檢測(cè)組件進(jìn)行了封裝忆谓,并提供了默認(rèn)動(dòng)畫和音效裆装,能夠非常方便導(dǎo)入到其他項(xiàng)目中。在這篇博客里倡缠,我將向大家分享一下我完成心率檢測(cè)的過程哨免,以及,期間我遇到的種種困難昙沦。
本文中涉及到的要點(diǎn)主要有:
- AVCapture
- Core Graphics
- Delegate & Block
- RGB -> HSV
- 帶通濾波
- 基音標(biāo)注算法(TP-Psola)
- 光電容積脈搏波描記法(PhotoPlethysmoGraphy, PPG)
在開始之前琢唾,我先為大家展示一下最后成品的效果:
上圖展示的是心率檢測(cè)過程中的主要界面。
在檢測(cè)的過程中盾饮,應(yīng)用能夠?qū)崟r(shí)捕捉心跳的波峰采桃,計(jì)算相應(yīng)的心率,并以Delegate或Block的形式回調(diào)丘损,在界面上顯示相應(yīng)的動(dòng)畫和音效普办。
〇、劇情概覽
好吧号俐,??其實(shí)上面的前情提要都是我瞎掰的泌豆,這個(gè)Demo是我來到公司的第一天接到的任務(wù)定庵。剛接到任務(wù)的時(shí)候其實(shí)是有點(diǎn)懵逼的吏饿,原本以為剛?cè)肼殐商炜赡芏际且纯次臋n,或者拖拖控件蔬浙,寫寫界面什么的猪落,結(jié)果Xcode都還沒裝好,突然接到一個(gè)心率檢測(cè)的任務(wù)畴博,頓時(shí)壓力就大起來了??笨忌,趕緊拍拍屁股起來找資料。
心率檢測(cè)的APP在我高三左右就有了俱病,我清楚地記得當(dāng)時(shí)官疲,年少無知的我還誤以為,大概又是哪個(gè)刁民閑著無聊惡搞的流氓應(yīng)用亮隙,特地下載下來試了一下途凫,沒想到居然真的能測(cè)。溢吻。维费。
當(dāng)時(shí)就震驚地打開了某度查了這類應(yīng)用的原理。所以現(xiàn)在找起資料來還是比較有方向性的。
花了一天的時(shí)間找資料犀盟,發(fā)現(xiàn)在手機(jī)心率檢測(cè)方面而晒,網(wǎng)上相關(guān)的東西還是比較少。不過各種資料參考下來阅畴,基本的實(shí)現(xiàn)思路已經(jīng)有了倡怎。
任務(wù)清單
- 實(shí)現(xiàn)心率檢測(cè)
一、整體思路
原理
首先說一說用手機(jī)攝像頭實(shí)現(xiàn)心率檢測(cè)所用到的原理贱枣。
我們知道诈胜,現(xiàn)在市面上有非常多具備心率檢測(cè)功能的可穿戴設(shè)備,比如各種手環(huán)以及各種Watch冯事,其實(shí)從本質(zhì)上講焦匈,我們這次要用到的原理跟這些可穿戴設(shè)備所用到的原理并無二致,它們都是基于光電容積脈搏波描記法(PhotoPlethysmoGraphy, PPG)昵仅。
PPG是追蹤可見光(通常為綠光)在人體組織中的反射缓熟。它具備一個(gè)可見光光源來照射皮膚,再使用光電傳感器采集被皮膚反射回來的光線摔笤。PPG有兩種模式够滑,透射式和反射式,像一般的手環(huán)手表這樣吕世,光源和傳感器在同一側(cè)的彰触,就是反射式;而醫(yī)院中常見的夾在指尖上的通常是透射式的命辖,即光源和傳感器在不同側(cè)况毅。
皮膚本身對(duì)光線的反射能力是相對(duì)穩(wěn)定的,但是心臟泵血使得血管容積周期性地變化尔艇,導(dǎo)致反射光也呈現(xiàn)出周期性的波動(dòng)值尔许,特別是在指尖這種毛細(xì)血管非常豐富的部位,這種周期性的波動(dòng)很容易被觀察到终娃。
使用iPhone的系統(tǒng)相機(jī)就可以輕易地用肉眼觀察到這種波動(dòng)——在錄像中打開閃光燈味廊,然后用手指輕輕覆蓋住攝像頭,就能觀察到滿屏的紅色圖像會(huì)隨著心跳產(chǎn)生一陣一陣的明暗變化棠耕,如下圖(請(qǐng)忽略滿屏的摩爾紋)余佛。
至于,為什么可穿戴設(shè)備上用的光源大多數(shù)都是綠光窍荧,我們用手機(jī)閃光燈的白光會(huì)不會(huì)有問題辉巡。這主要是因?yàn)榫G光在心率檢測(cè)中產(chǎn)生的信噪比比較大,有利于心率的檢測(cè)搅荞,用白光也是完全沒問題的红氯。詳情可以移步知乎:各種智能穿戴的心率檢測(cè)功能 框咙。我在這里就不細(xì)說了。
我的思路
我們已經(jīng)知道我們需要用閃光燈和攝像頭來充當(dāng)PPG的光源和傳感器痢甘,那么下面就來分析一下后續(xù)整體的方案喇嘱。下面是我搜集完數(shù)據(jù)之后大致畫出的一個(gè)流程圖。
- 首先我們需要采集相機(jī)的數(shù)據(jù)塞栅,這一步可以使用AVCapture者铜;
- 然后按照某種算法,對(duì)每一幀圖像計(jì)算出一個(gè)相應(yīng)的特征值并保存到數(shù)組中放椰,算法可以考慮取紅色分量或者轉(zhuǎn)換為HSV再計(jì)算作烟;
- 在得到一定量的數(shù)據(jù)后砾医,我們對(duì)這個(gè)時(shí)間段內(nèi)的數(shù)據(jù)進(jìn)行預(yù)處理压恒,譬如進(jìn)行濾波,過濾掉一些噪聲伦吠,可以參考一篇博客:巴特沃斯濾波器毛仪;
- 接下來借尿,就可以進(jìn)行心率計(jì)算路翻,這一步可能涉及到一些數(shù)字信號(hào)處理的內(nèi)容慨绳,例如波峰檢測(cè),信號(hào)頻率計(jì)算,可以使用Accelerate.Framework的vDSP處理框架讨韭,Accelerate框架的用法可以參考:StackOverFlow的一個(gè)回答(最終我并沒有使用,原因后面會(huì)提到)濒生;
- 最終就可以得到心率。
二、初步實(shí)現(xiàn)
有了大概的方案之后谁撼,我決定著手進(jìn)行實(shí)現(xiàn)了。
1)視頻流采集
我們前面已經(jīng)提到,我們要用AVCapture進(jìn)行視頻流的采集款咖。在使用AVCapture的時(shí)候,需要先建立AVCaptureSession富腊,相當(dāng)于是一個(gè)傳輸流是整,用來連接數(shù)據(jù)的輸入輸出,然后分別建立輸入和輸出的連接舵盈。因此,為了更加直觀,我先做了一個(gè)類似于相機(jī)的Demo句伶,把AVCapture采集到的相機(jī)圖像直接傳輸?shù)揭粋€(gè)Layer上。
-
創(chuàng)建AVCaptureSession
AVCaptureSession的配置過程類似于一次數(shù)據(jù)庫事務(wù)的提交。開始配置前必須調(diào)用[_session beginConfiguration];
來開始配置;完成所有的配置工作后酥筝,再調(diào)用[_session commitConfiguration];
來提交此次配置剿配。
因此,整個(gè)配置過程大致是這樣的:/** 建立輸入輸出流 */ _session = [AVCaptureSession new]; /** 開始配置AVCaptureSession */ [_session beginConfiguration]; /* * 配置session * (建立輸入輸出流) * ... */ /** 提交配置沪编,建立流 */ [_session commitConfiguration]; /** 開始傳輸數(shù)據(jù)流 */ [_session startRunning];
-
建立輸入流From Camera
要從相機(jī)建立輸入流厨幻,就得先獲取到照相機(jī)設(shè)備况脆,并且對(duì)它進(jìn)行相應(yīng)的配置饭宾。這里對(duì)照相機(jī)的配置最關(guān)鍵的是要打開閃光燈常亮。此外格了,再設(shè)置一下白平衡看铆、對(duì)焦等參數(shù)的鎖定,來保證后續(xù)的檢測(cè)過程中盛末,不會(huì)因?yàn)橄鄼C(jī)的自動(dòng)調(diào)整而導(dǎo)致特征值不穩(wěn)定。/** 獲取照相機(jī)設(shè)備并進(jìn)行配置 */ AVCaptureDevice *device = [self getCameraDeviceWithPosition:AVCaptureDevicePositionBack]; if ([device isTorchModeSupported:AVCaptureTorchModeOn]) { NSError *error = nil; /** 鎖定設(shè)備以配置參數(shù) */ [device lockForConfiguration:&error]; if (error) { return; } [device setTorchMode:AVCaptureTorchModeOn]; [device unlockForConfiguration];//解鎖 }
需要注意的是宵荒,照相機(jī)Device的配置過程中岩臣,需要事先鎖定它,鎖定成功后才能進(jìn)行配置。并且,在配置閃光燈等參數(shù)前秃臣,必須事先判斷當(dāng)前設(shè)備是否支持相應(yīng)的閃光燈模式或其他功能蠢终,確保當(dāng)前設(shè)備支持才能夠進(jìn)行設(shè)置慌核。
此外碱呼,對(duì)于相機(jī)的配置,還有一點(diǎn)非常重要:記得調(diào)低閃光燈亮度!桅锄!
長(zhǎng)期打開閃光燈會(huì)使得電池發(fā)熱,這對(duì)電池是一種傷害厢塘。在我調(diào)試的過程中茶没,曾經(jīng)無數(shù)次調(diào)著調(diào)著忘了閃光燈還沒關(guān)探入,最后整只手機(jī)發(fā)熱到燙手的程度才發(fā)現(xiàn)覆积,直接進(jìn)化成小米~ 所以欣孤,盡量將閃光燈的亮度降低馋没,經(jīng)過我的測(cè)試,即使閃關(guān)燈亮度開到最小也能夠測(cè)得清晰的心率降传。
接下來就是利用配置好的device創(chuàng)建輸入流:
/** 建立輸入流 */
NSError *error = nil;
AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:device
error:&error];
if (error) {
NSLog(@"DeviceInput error:%@", error.localizedDescription);
return;
}
-
建立輸出流To AVCaptureVideoDataOutput
建立輸出流需要用到AVCaptureVideoDataOutput類篷朵。我們需要?jiǎng)?chuàng)建一個(gè)AVCaptureVideoDataOutput類并設(shè)置它的像素輸出格式為32位的BGRA格式,這似乎是iPhone相機(jī)的原始格式(經(jīng)@熊皮皮提出婆排,除了這種格式声旺,還有兩種YUV的格式)。后續(xù)我們讀取圖像Buffer中的像素時(shí)段只,也是按照這個(gè)順序(BGRA)去讀取像素點(diǎn)的數(shù)據(jù)腮猖。設(shè)置中需要用一個(gè)NSDictionary來作為參數(shù)。
我們還要設(shè)置AVCaptureVideoDataOutput的代理赞枕,并創(chuàng)建一個(gè)新的線程(FIFO)來給輸出流運(yùn)行澈缺。/** 建立輸出流 */ AVCaptureVideoDataOutput *videoDataOutput = [AVCaptureVideoDataOutput new]; NSNumber *BGRA32PixelFormat = [NSNumber numberWithInt:kCVPixelFormatType_32BGRA]; NSDictionary *rgbOutputSetting; rgbOutputSetting = [NSDictionary dictionaryWithObject:BGRA32PixelFormat forKey:(id)kCVPixelBufferPixelFormatTypeKey]; [videoDataOutput setVideoSettings:rgbOutputSetting]; // 設(shè)置像素輸出格式 [videoDataOutput setAlwaysDiscardsLateVideoFrames:YES]; // 拋棄延遲的幀 dispatch_queue_t videoDataOutputQueue = dispatch_queue_create("VideoDataOutputQueue", DISPATCH_QUEUE_SERIAL); [videoDataOutput setSampleBufferDelegate:self queue:videoDataOutputQueue];
-
連接到AVCaptureSession
建立完輸入輸出流坪创,就要將它們和AVCaptureSession連接起來啦!
這里需要注意的是姐赡,必須先判斷是否能夠添加莱预,再進(jìn)行添加操作,如下所示项滑。if ([_session canAddInput:deviceInput]) [_session addInput:deviceInput]; if ([_session canAddOutput:videoDataOutput]) [_session addOutput:videoDataOutput];
-
實(shí)現(xiàn)代理協(xié)議的方法依沮,獲取視頻幀
上面的步驟中,我們將self設(shè)為AVCaptureVideoDataOutput的delegate杖们,那么現(xiàn)在我們就要在self中實(shí)現(xiàn)AVCaptureVideoDataOutputSampleBufferDelegate的方法xxx didOutputSampleBuffer xxx
悉抵,這樣在視頻幀到達(dá)的時(shí)候我們就能夠在這個(gè)方法中獲取到它肩狂。#pragma mark - AVCaptureVideoDataOutputSampleBufferDelegate & Algorithm - (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection { /** 讀取圖像Buffer */ CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); // // 我們可以在這里 // 計(jì)算這一幀的 // 特征值摘完。。傻谁。 // /** 轉(zhuǎn)成位圖以便繪制到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]; }); }
做到這里孝治,我們已經(jīng)獲得了一個(gè)類似于相機(jī)的Demo,在屏幕上可以輸出攝像頭采集的畫面了审磁,接下來谈飒,我們就要在這個(gè)代理方法中對(duì)每一幀圖像進(jìn)行特征值的計(jì)算。
2)采樣(計(jì)算特征值)
采樣過程中态蒂,最關(guān)鍵的就是如何將一幅圖像轉(zhuǎn)換為一個(gè)對(duì)應(yīng)的特征值杭措。
我先將所有像素點(diǎn)轉(zhuǎn)換為一個(gè)像素點(diǎn)(RGB):
轉(zhuǎn)換成一個(gè)像素點(diǎn)之后担巩,我們只剩下RGB三個(gè)數(shù)值闭树,事情就變簡(jiǎn)單得多磺平。在設(shè)計(jì)采樣的算法的過程中汗贫,我進(jìn)行了許多種嘗試呀非。
我先試著簡(jiǎn)單地使用R湖苞、G片部、B分量中的其中一個(gè)直接作為信號(hào)輸入拱烁,結(jié)果都不理想疹瘦。
- HSV色彩空間
想到之前圖形學(xué)的課上有介紹過HSV色彩空間崩哩,是將顏色表示為色相、飽和度言沐、明度(Hue, Saturation, Value)三個(gè)數(shù)值邓嘹。
我想,既然肉眼都能觀察到圖像顏色的變化险胰,而RGB又沒有明顯的反映汹押,那HSV的三個(gè)維度中應(yīng)該有某個(gè)維度是能夠反映出它的變化的。我便試著轉(zhuǎn)換為HSV鸯乃,結(jié)果發(fā)現(xiàn)色相H隨脈搏的變化很明顯鲸阻!于是跋涣,我就先確定用H值來作為特征值。
我簡(jiǎn)單地用Core Graphics直接在圖像的Layer上畫出H數(shù)值的折線:
3)心率計(jì)算
為了使得曲線更加直觀鸟悴,我對(duì)特征值稍做處理陈辱,又改變了一下橫坐標(biāo)的比例,得到如下截圖∠钢睿現(xiàn)在心率信號(hào)穩(wěn)定以后沛贪,波峰已經(jīng)比較明顯了,我們開始進(jìn)行心率的計(jì)算震贵。
最初,我想到的是利用快速傅里葉變換(FFT)對(duì)信號(hào)數(shù)組進(jìn)行處理猩系。FFT可以將時(shí)域的信號(hào)轉(zhuǎn)換成頻域的信號(hào)媚送,也就得到了一段信號(hào)在各個(gè)頻率上的分布,這樣寇甸,我們就能通過判斷占比最大的頻率塘偎,就差不多能確定心率了。
但是可能由于我缺乏信號(hào)處理的相關(guān)知識(shí)拿霉,經(jīng)過將近兩天的研究吟秩,我還是看不懂跟高數(shù)課本一樣的文檔。绽淘。涵防。
于是我決定先用暴力的方法算出心率,等能用的Demo出來之后沪铭,看看效果如何壮池,再考慮研究算法的優(yōu)化。
通過上面的曲線伦意,我們可以看出火窒,在信號(hào)穩(wěn)定的時(shí)候,波峰還是比較清晰的驮肉。因此我想熏矿,我可以設(shè)置一個(gè)閾值,進(jìn)行波峰的檢測(cè)离钝,只要信號(hào)超過閾值票编,就判定該幀處于一個(gè)波峰。然后再設(shè)置一個(gè)狀態(tài)機(jī)卵渴,完成波峰波谷之間的狀態(tài)轉(zhuǎn)換慧域,就能檢測(cè)出波峰了。
因?yàn)閺腁VCapture得到的圖像幀數(shù)為30幀浪读,也就是說昔榴,每一幀代表1/30s的時(shí)間辛藻。那么我只需要數(shù)一數(shù)從第一個(gè)波峰到最后一個(gè)波峰之間,經(jīng)過了多少幀互订,檢測(cè)到了多少波峰吱肌,那么,就能算出每個(gè)波峰間的周期仰禽,也就能算出心率了氮墨。
這個(gè)想法非常簡(jiǎn)單,但是存在一個(gè)問題吐葵,那就是规揪,閾值的設(shè)置。波峰的凸起程度并不是恒定的温峭,有時(shí)明顯猛铅,有時(shí)微弱。因此诚镰,一個(gè)固定的閾值肯定不能滿足實(shí)際檢測(cè)的需求奕坟。于是我想到我們可以根據(jù)心跳曲線波動(dòng)的上下范圍,來實(shí)時(shí)確定一個(gè)合適的閾值清笨。我做了如下修改:
每次進(jìn)行心率計(jì)算的時(shí)候,先找出整個(gè)數(shù)組的極大和極小值刃跛,確定數(shù)據(jù)上下波動(dòng)的范圍抠艾。
然后,根據(jù)這個(gè)范圍的一個(gè)百分比桨昙,來確定閾值检号。
也就是說,一個(gè)特征值只有超過了整組數(shù)據(jù)的百分之多少蛙酪,它才會(huì)被判定為波峰齐苛。
根據(jù)這個(gè)方法,我每隔一段時(shí)間對(duì)數(shù)據(jù)進(jìn)行一遍檢測(cè)桂塞,在Demo中實(shí)現(xiàn)了心率的計(jì)算凹蜂,又對(duì)界面進(jìn)行了簡(jiǎn)單的實(shí)現(xiàn),大致的效果如下阁危。
使用的過程中還存在一定程度的誤檢率玛痊,不過總算是實(shí)現(xiàn)了心率檢測(cè)~ ??????
三、性能優(yōu)化
在我粗略實(shí)現(xiàn)了心率檢測(cè)的功能后狂打,Leader提出了對(duì)性能進(jìn)行優(yōu)化的要求擂煞,順便向我普及了一波Instruments的用法(以前我一直沒有用過??)。
任務(wù)清單
- 性能優(yōu)化
- 封裝組件(delegate或block的形式)趴乡;
- 提供兩種默認(rèn)動(dòng)畫对省;
我用Instrument分析了心率檢測(cè)過程中的CPU占用蝗拿,發(fā)現(xiàn)占用率很高,維持在50%~60%左右蒿涎。不過這在我的預(yù)料中蛹磺,因?yàn)槲业乃惴ù_實(shí)很暴力??——每幀的圖像是1920x1080尺寸的,在1/30秒內(nèi)同仆,要對(duì)這200多萬個(gè)像素點(diǎn)進(jìn)行遍歷計(jì)算萤捆,還要轉(zhuǎn)換成位圖顯示在layer上,隔一段時(shí)間還要計(jì)算一次心率俗批。俗或。。
我分析了CPU占用比較多的部分岁忘,歸納了幾個(gè)可以考慮優(yōu)化的方向
- 降低采樣范圍
- 降低采樣率
- 取消AV輸出
- 降低分辨率
- 改進(jìn)算法辛慰,去除冗余計(jì)算
- 降低采樣范圍
現(xiàn)在的采樣算法是對(duì)所有的像素點(diǎn)進(jìn)行一次采樣,我想著是否能夠縮小采樣的范圍干像,例如只對(duì)中間某塊區(qū)域采樣帅腌,但試驗(yàn)后我發(fā)現(xiàn),只對(duì)某塊區(qū)域采樣會(huì)使得檢測(cè)到的波峰變得模糊麻汰,說明個(gè)別區(qū)域的采樣并不具有代表性速客。
接著我又想到了一個(gè)新的辦法。我發(fā)現(xiàn)圖像中五鲫,臨近像素點(diǎn)的顏色差異很小溺职,那么我可以跳躍著采樣,每隔幾列位喂、每隔幾行采樣一次浪耘,這樣一方面可以減少工作量,一方面對(duì)采樣的效果的影響也可以減少塑崖。
跳躍著采樣
采樣的方式就像上圖展示的一樣七冲,再設(shè)置一個(gè)常量用來調(diào)節(jié)每次跳躍的間距。這樣一來规婆,理論上澜躺,每次占用的時(shí)間就可以降低為原來的1/n^2,大大減少聋呢。經(jīng)過幾次嘗試后苗踪,可以看到,采樣算法所在的函數(shù)的CPU占用比例由原來的31%降低到了14%了削锰。
在分析CPU占用時(shí)通铲,我發(fā)現(xiàn)在循環(huán)中對(duì)RGB分別累加時(shí),第一個(gè)R的運(yùn)算占用100倍以上的時(shí)間器贩。開始時(shí)以為可能是Red分量數(shù)值較大颅夺,計(jì)算難度大朋截,Leader建議我使用位運(yùn)算,但是我改成位運(yùn)算后吧黄,瓶頸依舊存在部服,弄得我十分困惑。后來我試著把RGB的計(jì)算順序換一下拗慨,結(jié)果發(fā)現(xiàn)廓八,瓶頸和R無關(guān),不論RGB赵抢,只要誰在第一位剧蹂,誰就會(huì)成為瓶頸。后來我想到烦却,這應(yīng)該是CPU和內(nèi)存之間的數(shù)據(jù)傳輸造成的瓶頸宠叼,因?yàn)橄袼攸c(diǎn)都存在一塊很大的內(nèi)存塊里,在取第一個(gè)數(shù)據(jù)的時(shí)候可能速度比較慢其爵,然后后面取臨近數(shù)據(jù)的時(shí)候可能就有Cache了冒冬,所以速度回提高兩個(gè)數(shù)量級(jí)。
- 降低采樣率
降低采樣率就是將視頻的幀數(shù)降低摩渺,我記得简烤,不知道是香農(nóng)還是誰,有一個(gè)定理证逻,大概的意思就是說乐埠,采樣率只要達(dá)到頻率的兩倍以上,就能檢測(cè)出信號(hào)的頻率囚企。
(經(jīng)coderMoe童鞋指出,此處正式名稱應(yīng)為“耐奎斯特采樣定理”~香農(nóng)是參與者之一)
人的心跳上限一般是160/分鐘瑞眼,也就是不到3Hz龙宏,那理論上,我們的采樣率只要達(dá)到6幀/秒伤疙,就能夠計(jì)算出頻率银酗。
不過,由于我之前使用的算法還不是特別穩(wěn)定徒像,所以黍特,當(dāng)時(shí)我沒有對(duì)采樣率進(jìn)行改變。
取消AV輸出
之前我為了方便看效果锯蛀,將采集到的視頻圖像輸出到了界面上的一層Layer上灭衷,其實(shí)這個(gè)畫面完全沒必要顯示出來。因此我去除了這部分的功能旁涤,這樣一來翔曲,整體的CPU占用就降低到了33%以下迫像。-
降低分辨率
目前我們采集視頻的大小是1920x1080,其實(shí)我們并不需要分辨率這么高瞳遍。降低分辨率一方面可以減少需要計(jì)算的像素點(diǎn)闻妓,另一方面可以減少IO的時(shí)間。
在我將分辨率降低到640x480:if ([_session canSetSessionPreset:AVCaptureSessionPreset640x480]) { /** 降低圖像采集的分辨率 */ [_session setSessionPreset:AVCaptureSessionPreset640x480]; }
結(jié)果非常驚人掠械,整體的CPU占用率直接降低到了5%左右由缆!
- 改進(jìn)算法,去除冗余計(jì)算
最后猾蒂,我對(duì)算法中一些冗余的計(jì)算進(jìn)行了優(yōu)化均唉,不過,由于CPU占用已經(jīng)降低到了5%左右婚夫,真正的瓶頸已經(jīng)消除浸卦,所以這里的改進(jìn)并沒有很明顯的變化。
四案糙、封裝
此前限嫌,我們已經(jīng)完成了一個(gè)大致可用的心率監(jiān)測(cè)Demo,但在此之前时捌,我著重考慮的都是如何盡快實(shí)現(xiàn)心率檢測(cè)的功能怒医,對(duì)整體的結(jié)構(gòu)和對(duì)象的封裝都沒有太多的考慮,簡(jiǎn)直把OC的面向?qū)ο笥贸闪嗣嫦蜻^程奢讨。
那么我們接下來的一個(gè)重要任務(wù)稚叹,就是對(duì)我們的心率檢測(cè)進(jìn)行封裝,使它成為一個(gè)可復(fù)用的組件拿诸。
任務(wù)清單
- 封裝組件并提供合理接口(delegate或block的形式)扒袖;
- 提供兩種默認(rèn)動(dòng)畫;
封裝ViewController
最開始的時(shí)候亩码,我想到的是對(duì)ViewController進(jìn)行封裝季率,這樣別人有需要心率檢測(cè)的時(shí)候,就可以彈出一個(gè)心率監(jiān)測(cè)的ViewController描沟,上面帶有一些檢測(cè)過程中的動(dòng)畫效果飒泻,檢測(cè)完成后自動(dòng)dismiss,并且返回檢測(cè)到的心率吏廉。
我在protocol中聲明了三個(gè)接口:
/**
* 心率檢測(cè)ViewController的代理協(xié)議
*/
@protocol MTHeartBeatsCaptureViewControllerDelegate <NSObject>
@optional
- (void)heartBeatsCaptureViewController:(MTHeartBeatsCaptureViewController *)captureVC
didFinishCaptureHeartRate:(int)rate;
- (void)heartBeatsCaptureViewControllerDidCancel:(MTHeartBeatsCaptureViewController *)captureVC;
- (void)heartBeatsCaptureViewController:(MTHeartBeatsCaptureViewController *)captureVC
DidFailWithError:(NSError *)error;
@end
我將三個(gè)方法都設(shè)為了optional的泞遗,因?yàn)槲疫€在ViewController中設(shè)置了三個(gè)相應(yīng)的Block供外部使用,分別對(duì)應(yīng)三個(gè)方法席覆。
@property (nonatomic, copy)void(^didFinishCaptureHeartRateHandle)(int rate);
@property (nonatomic, copy)void(^didCancelCaptureHeartRateHandle)();
@property (nonatomic, copy)void(^didFailCaptureHeartRateHandle)(NSError *error);
封裝心率檢測(cè)類
對(duì)ViewController進(jìn)行封裝之后史辙,我們可以看到,還是比較不合理的。這意味著別人只能使用我們封裝起來的界面進(jìn)行心率檢測(cè)髓霞,如果使用組件的人有更好的交互方案卦睹,或者有特殊的邏輯需求,那他使用起來就會(huì)很不方便方库。因此结序,我們很有必要進(jìn)行更深層次的封裝。
接下來纵潦,我將會(huì)剝離出心率檢測(cè)的類徐鹤,進(jìn)行封裝。
首先邀层,我一點(diǎn)點(diǎn)剝離出心率檢測(cè)的關(guān)鍵代碼返敬,放進(jìn)新的MTHeartBeatsCapture
類中。剝離的差不多之后寥院,就發(fā)現(xiàn)滿屏的代碼都是紅色的Error??劲赠,花了一個(gè)下午,才把項(xiàng)目恢復(fù)到能運(yùn)行的狀態(tài)秸谢。
我在心率檢測(cè)類中設(shè)置了兩個(gè)方法:啟動(dòng)和停止凛澎。使用起來很方便。
/** 開始檢測(cè)心率 */
- (NSError *)start;
/** 停止檢測(cè)心率 */
- (void)stop;
然后估蹄,我重新設(shè)計(jì)了一個(gè)心率檢測(cè)器的回調(diào)接口塑煎,依舊是delegate和block并存的。新的接口如下:
/**
* 心率檢測(cè)器的代理協(xié)議;
* 可以選擇Delegate或者block來獲得通知,
* 因此protocol中所有方法均為可選方法
*/
@protocol MTHeartBeatsCaptureDelegate <NSObject>
@optional
/** 檢測(cè)到一次波峰(跳動(dòng)),可通過返回值選擇是否停止檢測(cè) */
- (BOOL)heartBeatsCapture:(MTHeartBeatsCapture *)capture heartBeatingWithRate:(int)rate;
/** 失去穩(wěn)定信號(hào) */
- (void)heartBeatsCaptureDidLost:(MTHeartBeatsCapture *)capture;
/** 得到新的特征值(30幀/秒) */
- (void)heartBeatsCaptureDataDidUpdata:(MTHeartBeatsCapture *)capture
@end
我在新的接口中加入了heartBeatsCaptureDidLost:
臭蚁,方便在特征值波動(dòng)劇烈的時(shí)候進(jìn)行回調(diào)最铁,這樣外部就能提醒用戶姿勢(shì)不對(duì)。而第三個(gè)方法垮兑,則是為了之后外部的動(dòng)畫view能夠做出類似于心電圖一樣的動(dòng)畫效果冷尉,而對(duì)外傳出數(shù)據(jù)。
我還移除了檢測(cè)成功的回調(diào)didFinishCaptureHeartRate:
系枪,換成了heartBeatingWithRate:
网严,把成功時(shí)機(jī)的判斷交給了外部,當(dāng)外部的開發(fā)人員認(rèn)為檢測(cè)的心率足夠穩(wěn)定了嗤无,就可以返回YES來停止檢測(cè)。
此外怜庸,我還移除了遇到錯(cuò)誤的回調(diào)DidFailWithError:
当犯,因?yàn)槲野l(fā)現(xiàn),幾乎所有可能遇到的錯(cuò)誤割疾,都是發(fā)生在開始前的準(zhǔn)備階段嚎卫,因此,我改成了在start
方法中返回錯(cuò)誤信息,并且枚舉出錯(cuò)誤類型作為code拓诸,封裝成NSError侵佃。
typedef NS_OPTIONS(NSInteger, CaptureError) {
CaptureErrorNoError = 0, /**< 沒有錯(cuò)誤 */
CaptureErrorNoAuthorization = 1 << 0, /**< 沒有照相機(jī)權(quán)限 */
CaptureErrorNoCamera = 1 << 1, /**< 不支持照相機(jī)設(shè)備,很可能處于模擬器上 */
CaptureErrorCameraConnectFailed = 1 << 2, /**< 相機(jī)出錯(cuò)奠支,無法連接到照相機(jī) */
CaptureErrorCameraConfigFailed = 1 << 3, /**< 照相機(jī)配置失敗馋辈,照相機(jī)可能被其他程序鎖定 */
CaptureErrorTimeOut = 1 << 4, /**< 檢測(cè)超時(shí),此時(shí)應(yīng)提醒用戶正確放置手指 */
CaptureErrorSetupSessionFailed = 1 << 5, /**< 視頻數(shù)據(jù)流建立失敗 */
};
主要的工作完成后倍谜,Leader給我提了不少意見迈螟,主要還是封裝上存在的一些問題,很多地方?jīng)]有必要對(duì)外公開尔崔,應(yīng)該盡可能地對(duì)外隱藏答毫,接口也應(yīng)該盡量地精簡(jiǎn),沒必要的功能要盡可能的去掉季春。特別是對(duì)外公開的一個(gè)特征值數(shù)組(NSMutableArray)洗搂,對(duì)外應(yīng)該不可變,這一點(diǎn)我一直沒有考慮到载弄。
封裝動(dòng)畫&改進(jìn)動(dòng)畫
心率檢測(cè)類封裝完成后耘拇,我又剝離出顯示心跳波形的部分,封裝成一個(gè)MTHeartBeatsWaveView
侦锯,使用的時(shí)候只要將動(dòng)畫View賦給MTHeartBeatsCapture
作為delegate驼鞭,該view上就能獲取到特征值數(shù)據(jù)并進(jìn)行顯示。
動(dòng)畫改進(jìn):在測(cè)試的過程中尺碰,我發(fā)現(xiàn)波形動(dòng)畫顯示的波形不太理想挣棕,View的大小是初始化的時(shí)候就確定的,但是心跳波動(dòng)的幅度變化是比較大的亲桥,有時(shí)候一馬平川洛心,堪比飛機(jī)場(chǎng),有時(shí)候波瀾壯闊题篷,直接超出View的范圍词身。
因此我對(duì)動(dòng)畫的顯示做了一個(gè)改進(jìn):能夠根據(jù)當(dāng)前波形的范圍,計(jì)算出合適的縮放比番枚,對(duì)心跳曲線的Y坐標(biāo)進(jìn)行動(dòng)態(tài)的縮放法严,使它的上下幅度適合當(dāng)前的View。
這個(gè)改進(jìn)大大提高了用戶體驗(yàn)葫笼。
五深啤、優(yōu)化
我們可以看到,先前得到的曲線已經(jīng)能較好地反映出心臟的搏動(dòng)路星,但是現(xiàn)在進(jìn)行心率的計(jì)算還是存在一定的誤檢率溯街。上圖中展示的清晰的心跳曲線,實(shí)際上是比較理想的時(shí)候,測(cè)試中會(huì)發(fā)現(xiàn)呈昔,采樣得到的數(shù)據(jù)經(jīng)常存在較大的噪聲和擾動(dòng)挥等,導(dǎo)致心率計(jì)算中經(jīng)常會(huì)有波峰的誤判。因此堤尾,我在以下兩方面做了優(yōu)化肝劲,來提高心率檢測(cè)的準(zhǔn)確度。
1哀峻、在預(yù)處理環(huán)節(jié)進(jìn)行濾波
分析一下心率曲線里的噪聲涡相,我們會(huì)發(fā)現(xiàn),噪聲中含有一些高頻噪聲剩蟀,這部分噪聲可能是手指的細(xì)微抖動(dòng)造成的催蝗,也可能是相機(jī)產(chǎn)生的一些噪點(diǎn)。因此育特,我找到了一個(gè)簡(jiǎn)易的實(shí)時(shí)的帶通濾波器丙号,對(duì)之前我們采樣獲得到的H值進(jìn)行處理,濾除了一部分高頻和低頻的噪聲缰冤。
在經(jīng)過濾波器的處理之后犬缨,我們得到的曲線就更加平滑啦。
2棉浸、參考TP-Psola算法怀薛,排除偽波峰
經(jīng)過濾波器的處理之后,我們會(huì)發(fā)現(xiàn)迷郑,在每個(gè)心跳周期中枝恋,總會(huì)有一個(gè)小波峰,因?yàn)樗皇钦嬲牟ǚ逦撕Γ虼宋曳Q它為“偽波峰”焚碌,這個(gè)偽波峰非常明顯,有時(shí)也會(huì)干擾到我們心率的檢測(cè)霸妹,被算法誤判為心跳波峰十电,導(dǎo)致心率直接翻倍。
這個(gè)偽波峰出現(xiàn)是因?yàn)樘久送獠康脑肼曋饩槁睿呐K本身的跳動(dòng)周期中也會(huì)出現(xiàn)許多的“雜波”。我們來看一次心跳的完整過程罢绽。
上圖是一次心跳周期中偎漫,心臟的狀態(tài)變化以及對(duì)應(yīng)產(chǎn)生的波段∮欣拢可以看到,在心臟收縮前后,人體也會(huì)有電信號(hào)刺激心臟舒張棚壁,這在心電圖上會(huì)表現(xiàn)出若干次的波動(dòng)杯矩。而血壓也會(huì)有相應(yīng)的變化,我們檢測(cè)到的數(shù)據(jù)的波動(dòng)就是這樣形成的袖外。
因此史隆,這個(gè)偽波峰的形成是無法避免的,現(xiàn)有的通過閾值來判斷波峰的方法很容易被欺騙曼验,還是要考慮算法的改進(jìn)泌射,因此我又想到了快速傅里葉變換。
由于我對(duì)信號(hào)處理知之甚少鬓照,我看了兩天的快速傅里葉熔酷,還是沒有進(jìn)展。于是我請(qǐng)教了部門里的前輩們豺裆,大家非常熱情拒秘,推薦了不少方案和資料。其中一位實(shí)驗(yàn)室音頻處理的博偉學(xué)長(zhǎng)臭猜,碰巧在新人入職培訓(xùn)時(shí)和我分到了同一組躺酒,我就趁著閑暇的時(shí)候請(qǐng)教了他一些相關(guān)的問題。他覺得心率的波形比較簡(jiǎn)單蔑歌,沒必要用快速傅里葉變換羹应,并且向我推薦了基音檢測(cè)算法。
簡(jiǎn)單地說次屠,這個(gè)算法會(huì)標(biāo)注出可能的波峰园匹,然后通過動(dòng)態(tài)規(guī)劃排除掉偽波峰,就能得到真正的波峰啦帅矗。我根據(jù)這個(gè)算法的思路偎肃,實(shí)現(xiàn)了一個(gè)簡(jiǎn)化版的偽波峰排除算法。經(jīng)過改進(jìn)后的心率檢測(cè)浑此,經(jīng)測(cè)試準(zhǔn)確度達(dá)到了和Apple watch差不多的程度累颂。(自我感覺良好??,求輕噴~~)
實(shí)時(shí)波峰檢測(cè)
我還希望提供一個(gè)實(shí)時(shí)的心跳動(dòng)畫凛俱,因此我還實(shí)現(xiàn)了一個(gè)實(shí)時(shí)的波峰檢測(cè)紊馏。這樣每次檢測(cè)到一個(gè)波峰之后,就可以立刻通知delegate或者block蒲犬,在界面上做出動(dòng)畫朱监。
歇-后-語
由于這一章節(jié)是歇了一陣子之后才寫的,因此我把它叫做——歇后語原叮。
這個(gè)心率檢測(cè)的項(xiàng)目前后一共做了三個(gè)禮拜左右赫编,雖然第一個(gè)Demo用了三四天就完成巡蘸,但是后續(xù)的封裝和優(yōu)化卻用了兩個(gè)星期的時(shí)間,嗯擂送,感觸頗深悦荒。。嘹吨。
從最開始的incredible搬味,到最后的好意思說堪比Apple Watch,真的是一個(gè)很有成就感的過程蟀拷。雖然期間遇到了不少困難碰纬,甚至有那么一兩次覺得自己真的無解了,但到最后總能熬過去问芬,山重水復(fù)疑無路悦析,柳暗花明又一村。真的忍不住要念詩了愈诚,感覺很充實(shí)她按,很開心。
在做這個(gè)項(xiàng)目的過程中炕柔,我也得到了許多人的幫助酌泰。部門里的各位前輩、同事匕累,在看到我的提問之后陵刹,非常熱情地向我提供意見和資料。希望這篇博客會(huì)對(duì)大家有所幫助欢嘿。謝謝大家~
【更新于2016/8/10】
經(jīng)coderMoe童鞋指出衰琐,文中 [三、2.降低采樣率] 提到的 “定理” 正式的名稱為“奈奎斯特采樣定理”炼蹦。
感謝這段時(shí)間以來羡宙,大家的鼓勵(lì)和支持,前陣子我寫這篇文章的時(shí)候掐隐,是萬萬沒有想到會(huì)得到這么多人的關(guān)注的狗热,實(shí)在是受寵若驚。有很多人詳細(xì)地閱讀了這篇博客虑省,并且提出了重要的意見匿刮,甚至還有幾位客官打賞了我(但是簡(jiǎn)書取現(xiàn)要滿100RMB才行,所以目前我還無法享用這筆增肥基金??哈哈)探颈,真的很感謝你們熟丸。
我當(dāng)時(shí)寫這篇博客也花了不少時(shí)間,只怪我語文沒學(xué)好伪节,在言辭表達(dá)上光羞、邏輯結(jié)構(gòu)上绩鸣,沒能做得更好,大家如果有什么意見建議狞山、或者不同的見解全闷,希望能不吝賜教~~大家的關(guān)注和交流會(huì)讓我更有動(dòng)力分享博客,要知道萍启,寫作對(duì)我這種工科生而言,真的是屏鳍,“”體力活“”勘纯。??
有想要進(jìn)一步關(guān)注我的朋友,可以收藏一下我正在搭建的博客钓瞭,域名正在備案中驳遵,不過博客系統(tǒng)是已經(jīng)搭起來了,有興趣的朋友請(qǐng)移步:punmy.cn??
另外山涡,關(guān)于許多朋友非常關(guān)心的開源的問題堤结,這兩天上班比較忙,但是我會(huì)在近期確定是否開源鸭丛,屆時(shí)會(huì)通過簡(jiǎn)書更新竞穷,感謝關(guān)注!
【更新于2016/8/18】
感謝大家的厚愛鳞溉,收到Leader的回復(fù)瘾带,這個(gè)項(xiàng)目暫時(shí)不開源,不好意思熟菲。
但是大家如果有什么問題看政,歡迎繼續(xù)和我探討!??
【更新于2016/8/19】
我的域名審核通過啦抄罕,歡迎訪問:punmy.cn??
另外允蚣,有朋友指出,iPhone相機(jī)支持的原始數(shù)據(jù)格式有三種呆贿,一種是文中提到的BGRA嚷兔,另兩種似乎是YUV的格式,我對(duì)這方面不太了解榨崩,感謝提出谴垫,詳情請(qǐng)看文檔。
此外母蛛,有個(gè)別同學(xué)翩剪,不經(jīng)大腦不經(jīng)谷歌,就一味指責(zé)我文中的圖片造假彩郊。
說什么前弯,文中提到的“系統(tǒng)相機(jī)就可以明顯觀察到明暗變化”的那張照片蚪缀,根本不可能拍出清晰的指紋。恕出。询枚。
拜托,各位大爺浙巫,那是摩爾紋金蜀,誰告訴你是指紋了?的畴?渊抄?excuse me?丧裁?不明白請(qǐng)谷歌护桦,動(dòng)不動(dòng)就罵人,我真是謝(qu)謝(ni)你(da)們(ye)了煎娇。
不過也因此收到了一些朋友的打賞??,謝謝大家了~
再次聲明英岭,歡迎理性探討,拒絕BB~
The End.
延伸閱讀
- 光電容積脈搏波描記法”
https://en.wikipedia.org/wiki/Photoplethysmogram - 濾波:http://blog.csdn.net/shengzhadon/article/details/46803401
- Accelerate.framework:DSP處理框架(vDSP):
http://stackoverflow.com/questions/3398753/using-the-apple-fft-and-accelerate-framework - core Graphics畫圖:http://www.mamicode.com/info-detail-841887.html
- 心率檢測(cè)論文:http://www.doc88.com/p-0307201762779.html
- 通過臉部識(shí)別心率:http://people.csail.mit.edu/mrub/vidmag/
外部引用
[0]: 寫出“前情提要”的時(shí)候睡陪,腦子里蹦出的是:previously on marvel agents of shield??
[1]: 引用自維基百科,由Kalumet - selbst erstellt = 自己的作品匿情,<a 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 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 title="Creative Commons Attribution-Share Alike 3.0">CC BY-SA 3.0</a>据德,https://commons.wikimedia.org/w/index.php?curid=943857