心跳之旅—??—iOS用手機(jī)攝像頭檢測(cè)心率(PPG)

簡(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è)的ViewController

上圖展示的是心率檢測(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)昵仅。

iWatch的心率傳感器發(fā)出的綠光

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)忽略滿屏的摩爾紋)余佛。

直接用肉眼就能觀察到相機(jī)圖像的明暗變化

至于,為什么可穿戴設(shè)備上用的光源大多數(shù)都是綠光窍荧,我們用手機(jī)閃光燈的白光會(huì)不會(huì)有問題辉巡。這主要是因?yàn)榫G光在心率檢測(cè)中產(chǎn)生的信噪比比較大,有利于心率的檢測(cè)搅荞,用白光也是完全沒問題的红氯。詳情可以移步知乎:各種智能穿戴的心率檢測(cè)功能 框咙。我在這里就不細(xì)說了。

我的思路

我們已經(jīng)知道我們需要用閃光燈和攝像頭來充當(dāng)PPG的光源和傳感器痢甘,那么下面就來分析一下后續(xù)整體的方案喇嘱。下面是我搜集完數(shù)據(jù)之后大致畫出的一個(gè)流程圖。

整體思路
  1. 首先我們需要采集相機(jī)的數(shù)據(jù)塞栅,這一步可以使用AVCapture者铜;
  2. 然后按照某種算法,對(duì)每一幀圖像計(jì)算出一個(gè)相應(yīng)的特征值并保存到數(shù)組中放椰,算法可以考慮取紅色分量或者轉(zhuǎn)換為HSV再計(jì)算作烟;
  3. 在得到一定量的數(shù)據(jù)后砾医,我們對(duì)這個(gè)時(shí)間段內(nèi)的數(shù)據(jù)進(jìn)行預(yù)處理压恒,譬如進(jìn)行濾波,過濾掉一些噪聲伦吠,可以參考一篇博客:巴特沃斯濾波器毛仪;
  4. 接下來借尿,就可以進(jìn)行心率計(jì)算路翻,這一步可能涉及到一些數(shù)字信號(hào)處理的內(nèi)容慨绳,例如波峰檢測(cè),信號(hào)頻率計(jì)算,可以使用Accelerate.Framework的vDSP處理框架讨韭,Accelerate框架的用法可以參考:StackOverFlow的一個(gè)回答(最終我并沒有使用,原因后面會(huì)提到)濒生;
  5. 最終就可以得到心率。

二、初步實(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上。

  1. 創(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];       
    
  2. 建立輸入流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;
    }
  1. 建立輸出流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];
    
  2. 連接到AVCaptureSession
    建立完輸入輸出流坪创,就要將它們和AVCaptureSession連接起來啦!
    這里需要注意的是姐赡,必須先判斷是否能夠添加莱预,再進(jìn)行添加操作,如下所示项滑。

     if ([_session canAddInput:deviceInput])
         [_session addInput:deviceInput];
     if ([_session canAddOutput:videoDataOutput])
         [_session addOutput:videoDataOutput];
    
  3. 實(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):

累加合成一個(gè)像素點(diǎn)

轉(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ù)值邓嘹。

HSV色彩空間**[5]**

我想,既然肉眼都能觀察到圖像顏色的變化险胰,而RGB又沒有明顯的反映汹押,那HSV的三個(gè)維度中應(yīng)該有某個(gè)維度是能夠反映出它的變化的。我便試著轉(zhuǎn)換為HSV鸯乃,結(jié)果發(fā)現(xiàn)色相H隨脈搏的變化很明顯鲸阻!于是跋涣,我就先確定用H值來作為特征值。

我簡(jiǎn)單地用Core Graphics直接在圖像的Layer上畫出H數(shù)值的折線:

色相H隨脈搏的變化

3)心率計(jì)算

為了使得曲線更加直觀鸟悴,我對(duì)特征值稍做處理陈辱,又改變了一下橫坐標(biāo)的比例,得到如下截圖∠钢睿現(xiàn)在心率信號(hào)穩(wěn)定以后沛贪,波峰已經(jīng)比較明顯了,我們開始進(jìn)行心率的計(jì)算震贵。

縮放后利赋,穩(wěn)定的時(shí)候的心率信號(hào)

最初,我想到的是利用快速傅里葉變換(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è)Demo

使用的過程中還存在一定程度的誤檢率玛痊,不過總算是實(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ì)算
  1. 降低采樣范圍
    現(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í)。

  1. 降低采樣率
    降低采樣率就是將視頻的幀數(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)行濾波

得到的曲線有時(shí)含有比較多的噪聲

分析一下心率曲線里的噪聲涡相,我們會(huì)發(fā)現(xiàn),噪聲中含有一些高頻噪聲剩蟀,這部分噪聲可能是手指的細(xì)微抖動(dòng)造成的催蝗,也可能是相機(jī)產(chǎn)生的一些噪點(diǎn)。因此育特,我找到了一個(gè)簡(jiǎn)易的實(shí)時(shí)的帶通濾波器丙号,對(duì)之前我們采樣獲得到的H值進(jìn)行處理,濾除了一部分高頻和低頻的噪聲缰冤。

加入濾波器處理后的心率信號(hào)

在經(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)許多的“雜波”。我們來看一次心跳的完整過程罢绽。

心電圖波形產(chǎn)生過程的動(dòng)畫 **[1]**

上圖是一次心跳周期中偎漫,心臟的狀態(tài)變化以及對(duì)應(yīng)產(chǎn)生的波段∮欣拢可以看到,在心臟收縮前后,人體也會(huì)有電信號(hào)刺激心臟舒張棚壁,這在心電圖上會(huì)表現(xiàn)出若干次的波動(dòng)杯矩。而血壓也會(huì)有相應(yīng)的變化,我們檢測(cè)到的數(shù)據(jù)的波動(dòng)就是這樣形成的袖外。

正常心電周期 **[2]**

因此史隆,這個(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è)算法。

基音標(biāo)注

簡(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)畫朱监。

心率檢測(cè)的ViewController

歇-后-語

由于這一章節(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.


延伸閱讀

外部引用

[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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市跷车,隨后出現(xiàn)的幾起案子棘利,更是在濱河造成了極大的恐慌,老刑警劉巖朽缴,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件善玫,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡密强,警方通過查閱死者的電腦和手機(jī)茅郎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門蜗元,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人系冗,你說我怎么就攤上這事奕扣。” “怎么了掌敬?”我有些...
    開封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵惯豆,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我奔害,道長(zhǎng)循帐,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任舀武,我火速辦了婚禮,結(jié)果婚禮上离斩,老公的妹妹穿的比我還像新娘银舱。我一直安慰自己,他們只是感情好跛梗,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開白布寻馏。 她就那樣靜靜地躺著,像睡著了一般核偿。 火紅的嫁衣襯著肌膚如雪诚欠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天漾岳,我揣著相機(jī)與錄音轰绵,去河邊找鬼。 笑死尼荆,一個(gè)胖子當(dāng)著我的面吹牛左腔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播捅儒,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼液样,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了巧还?” 一聲冷哼從身側(cè)響起鞭莽,我...
    開封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎麸祷,沒想到半個(gè)月后澎怒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡摇锋,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年丹拯,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了站超。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡乖酬,死狀恐怖死相,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情咬像,我是刑警寧澤算撮,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站县昂,受9級(jí)特大地震影響肮柜,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜倒彰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一审洞、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧待讳,春花似錦芒澜、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至琳彩,卻和暖如春誊酌,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背露乏。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工碧浊, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人施无。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓辉词,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親猾骡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子瑞躺,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,163評(píng)論 25 707
  • 最近在封裝一個(gè)手機(jī)攝像頭測(cè)心率的模塊,搞得精神各種緊張兴想,導(dǎo)致吃飯幢哨、路上、做夢(mèng)嫂便,甚至都在想這個(gè)東西捞镰,就在剛剛終于搞完...
    YvanLiu閱讀 10,252評(píng)論 42 115
  • WebSocket-Swift Starscream的使用 WebSocket 是 HTML5 一種新的協(xié)議。它實(shí)...
    香橙柚子閱讀 23,869評(píng)論 8 183
  • 紅豺 今天花了40分鐘看完了一本306頁的書名字叫紅豺。內(nèi)容講的是紅豺火燒云...
    夢(mèng)境里的冬天閱讀 313評(píng)論 0 1
  • 騰沖和順柏聯(lián)精品酒店位于騰沖和順鎮(zhèn)張家坡岸售。這里的溫泉占地面積50畝践樱,是一個(gè)露天園林溫泉,還有19間獨(dú)棟湯屋凸丸,擁有騰...
    攜程攻略社區(qū)閱讀 590評(píng)論 0 0