原理
簡單介紹一下,網上可以查到很多關于手機測心率的這種項目,大概就是: 把手指放在攝像頭和閃光燈上,通過手指處脈搏跳動充血導致的細微顏色變化來確定心跳波動,確定波峰波谷,根據兩個波峰之間的時間差來確定瞬時心率。
思路
首先,采集視頻流,根據拿到的RGB顏色轉成HSV顏色集,其實我們只用到了HSV的H。
對拿到的H進行一些處理,看跟人喜好或者具體情況,主要是用于后面的折線圖和計算瞬時心率,如果有能力的話可以處理一下噪音數據,因為可能測的時候手指輕微抖動會造成一些不穩定的數據。
根據處理后的H就可以進行畫折線圖了,我是把處理后的H和時間戳進行了綁定,用來后面的計算心率。
根據處理后的H來確定波峰波谷,利用兩個波谷之間的時間差計算心率。
實現
大致思路就是上面這樣,下面來看一下代碼具體實現以下。
1.首先我先初始化了一些數據,方便后面使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// 設備 @property (strong, nonatomic) AVCaptureDevice *device; // 結合輸入輸出 @property (strong, nonatomic) AVCaptureSession *session; // 輸入設備 @property (strong, nonatomic) AVCaptureDeviceInput *input; // 輸出設備 @property (strong, nonatomic) AVCaptureVideoDataOutput *output; // 輸出的所有點 @property (strong, nonatomic) NSMutableArray *points; // 記錄浮點變化的前一次的值 static float lastH = 0; // 用于判斷是否是第一個福點值 static int count = 0; // 初始化 self.device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; self.session = [[AVCaptureSession alloc]init]; self.input = [[AVCaptureDeviceInput alloc]initWithDevice:self.device error:nil]; self.output = [[AVCaptureVideoDataOutput alloc]init]; self.points = [[NSMutableArray alloc]init]; |
2.設置視頻采集流,為了節省內存,我沒有輸出視頻畫面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
// 開啟閃光燈 if ([self.device isTorchModeSupported:AVCaptureTorchModeOn]) { [self.device lockForConfiguration:nil]; // 開啟閃光燈 self.device.torchMode=AVCaptureTorchModeOn; // 調低閃光燈亮度(為了減少內存占用和避免時間長手機發燙) [self.device setTorchModeOnWithLevel:0.01 error:nil]; [self.device unlockForConfiguration]; } // 開始配置input output [self.session beginConfiguration]; // 設置像素輸出格式 NSNumber *BGRA32Format = [NSNumber numberWithInt:kCVPixelFormatType_32BGRA]; NSDictionary *setting =@{(id)kCVPixelBufferPixelFormatTypeKey:BGRA32Format}; [self.output setVideoSettings:setting]; // 拋棄延遲的幀 [self.output setAlwaysDiscardsLateVideoFrames:YES]; //開啟攝像頭采集圖像輸出的子線程 dispatch_queue_t outputQueue = dispatch_queue_create( "VideoDataOutputQueue" , DISPATCH_QUEUE_SERIAL); // 設置子線程執行代理方法 [self.output setSampleBufferDelegate:self queue:outputQueue]; // 向session添加 if ([self.session canAddInput:self.input]) [self.session addInput:self.input]; if ([self.session canAddOutput:self.output]) [self.session addOutput:self.output]; // 降低分辨率,減少采樣率(為了減少內存占用) self.session.sessionPreset = AVCaptureSessionPreset1280x720; // 設置最小的視頻幀輸出間隔 self.device.activeVideoMinFrameDuration = CMTimeMake(1, 10); // 用當前的output 初始化connection AVCaptureConnection *connection =[self.output connectionWithMediaType:AVMediaTypeVideo]; [connection setVideoOrientation:AVCaptureVideoOrientationPortrait]; // 完成編輯 [self.session commitConfiguration]; // 開始運行 [self.session startRunning]; |
這里我降低了閃光燈亮度,降低了分辨率,減少了每秒鐘輸出的幀。主要就是為了減少內存的占用。(我手里只有一臺6,沒有測其他設備可不可以)
3.在output的代理方法中采集視頻流
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
// captureOutput->當前output sampleBuffer->樣本緩沖 connection->捕獲連接 - ( void )captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection { //獲取圖層緩沖 CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); CVPixelBufferLockBaseAddress(imageBuffer, 0); uint8_t*buf = (uint8_t *)CVPixelBufferGetBaseAddress(imageBuffer); size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer); size_t width = CVPixelBufferGetWidth(imageBuffer); size_t height = CVPixelBufferGetHeight(imageBuffer); float r = 0, g = 0,b = 0; float h,s,v; // 計算RGB TORGB(buf, width, height, bytesPerRow, &r, &g, &b); // RGB轉HSV RGBtoHSV(r, g, b, &h, &s, &v); // 獲取當前時間戳(精確到毫秒) double t = [[NSDate date] timeIntervalSince1970]*1000; // 返回處理后的浮點值 float p = HeartRate(h); // 綁定浮點和時間戳 NSDictionary *point = @{[NSNumber numberWithDouble:t]:[NSNumber numberWithFloat:p]}; //下面按個人情況可以進行計算心率或者畫心率圖 } |
到這里數據已經處理好了,后面可以根據數據畫折線圖,或者計算心率
計算RGB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
void TORGB (uint8_t *buf, float ww, float hh, size_t pr, float *r, float *g, float *b) { float wh = ( float )(ww * hh ); for ( int y = 0; y < hh; y++) { for ( int x = 0; x < ww * 4; x += 4) { *b += buf[x]; *g += buf[x+1]; *r += buf[x+2]; } buf += pr; } *r /= 255 * wh; *g /= 255 * wh; *b /= 255 * wh; } |
RGB轉HSV
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
void RGBtoHSV( float r, float g, float b, float *h, float *s, float *v ) { float min, max, delta; min = MIN( r, MIN(g, b )); max = MAX( r, MAX(g, b )); *v = max; delta = max - min; if ( max != 0 ) *s = delta / max; else { *s = 0; *h = -1; return ; } if ( r == max ) *h = ( g - b ) / delta; else if ( g == max ) *h = 2 + (b - r) / delta; else *h = 4 + (r - g) / delta; *h *= 60; if ( *h < 0 ) *h += 360; } |
根據h處理浮點
1
2
3
4
5
6
7
8
|
float HeartRate ( float h) { float low = 0; count++; lastH = (count==1)?h:lastH; low = (h-lastH); lastH = h; return low; } |
4.分析數據,計算心率
這里我糾結了好長時間,試了幾種不同的方法,都沒有一個比較理想的結果,計算出來的特別不準。后來看了 http://ios.jobbole.com/88158/ 這篇文章,后面優化的部分有一個 基音算法 ,雖不明,但覺厲,對此表示非常感謝。吼吼吼。
原理:就是說劃一個時間段,在這個時間段里面找到一個 最低峰值 ,然后確定一個 周期 ,然后分別在 這個峰值 前間隔 0.5個周期 的 1周期里 和 這個峰值 后間隔 0.5個周期 的 1周期 里找到一個最低峰值。 然后根據這幾個值來確定瞬時心率。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
|
- ( void )analysisPointsWith:(NSDictionary *)point { [self.points addObject:point]; if (self.points.count<=30) return ; int count = ( int )self.points.count; if (self.points.count%10 == 0) { int d_i_c = 0; //最低峰值的位置 姑且算在中間位置 c->center int d_i_l = 0; //最低峰值左面的最低峰值位置 l->left int d_i_r = 0; //最低峰值右面的最低峰值位置 r->right float trough_c = 0; //最低峰值的浮點值 float trough_l = 0; //最低峰值左面的最低峰值浮點值 float trough_r = 0; //最低峰值右面的最低峰值浮點值 // 1.先確定數據中的最低峰值 for ( int i = 0; i < count; i++) { float trough = [[[self.points[i] allObjects] firstObject] floatValue]; if (trough < trough_c) { trough_c = trough; d_i_c = i; } } // 2.找到最低峰值以后 以最低峰值為中心 找到前0.5-1.5周期中的最低峰值 和后0.5-1.5周期的最低峰值 if (d_i_c >= 1.5*T) { // a.如果最低峰值處在中心位置, 即距離前后都至少有1.5個周期 if (d_i_c <= count-1.5*T) { // 左面最低峰值 for ( int j = d_i_c - 0.5*T; j > d_i_c - 1.5*T; j--) { float trough = [[[self.points[j] allObjects] firstObject] floatValue]; if (trough < trough_l) { trough_l = trough; d_i_l = j; } } // 右面最低峰值 for ( int k = d_i_c + 0.5*T; k < d_i_c + 1.5*T; k++) { float trough = [[[self.points[k] allObjects] firstObject] floatValue]; if (trough < trough_r) { trough_r = trough; d_i_r = k; } } } // b.如果最低峰值右面不夠1.5個周期 分兩種情況 不夠0.5個周期和夠0.5個周期 else { // b.1 夠0.5個周期 if (d_i_c <count-0.5*T) { // 左面最低峰值 for ( int j = d_i_c - 0.5*T; j > d_i_c - 1.5*T; j--) { float trough = [[[self.points[j] allObjects] firstObject] floatValue]; if (trough < trough_l) { trough_l = trough; d_i_l = j; } } // 右面最低峰值 for ( int k = d_i_c + 0.5*T; k < count; k++) { float trough = [[[self.points[k] allObjects] firstObject] floatValue]; if (trough < trough_r) { trough_r = trough; d_i_r = k; } } } // b.2 不夠0.5個周期 else { // 左面最低峰值 for ( int j = d_i_c - 0.5*T; j > d_i_c - 1.5*T; j--) { float trough = [[[self.points[j] allObjects] firstObject] floatValue]; if (trough < trough_l) { trough_l = trough; d_i_l = j; } } } } } // c. 如果左面不夠1.5個周期 一樣分兩種情況 夠0.5個周期 不夠0.5個周期 else { // c.1 夠0.5個周期 if (d_i_c>0.5*T) { // 左面最低峰值 for ( int j = d_i_c - 0.5*T; j > 0; j--) { float trough = [[[self.points[j] allObjects] firstObject] floatValue]; if (trough < trough_l) { trough_l = trough; d_i_l = j; } } // 右面最低峰值 for ( int k = d_i_c + 0.5*T; k < d_i_c + 1.5*T; k++) { float trough = [[[self.points[k] allObjects] firstObject] floatValue]; if (trough < trough_r) { trough_r = trough; d_i_r = k; } } } // c.2 不夠0.5個周期 else { // 右面最低峰值 for ( int k = d_i_c + 0.5*T; k < d_i_c + 1.5*T; k++) { float trough = [[[self.points[k] allObjects] firstObject] floatValue]; if (trough < trough_r) { trough_r = trough; d_i_r = k; } } } } // 3. 確定哪一個與最低峰值更接近 用最接近的一個最低峰值測出瞬時心率 60*1000兩個峰值的時間差 if (trough_l-trough_c < trough_r-trough_c) { NSDictionary *point_c = self.points[d_i_c]; NSDictionary *point_l = self.points[d_i_l]; double t_c = [[[point_c allKeys] firstObject] doubleValue]; double t_l = [[[point_l allKeys] firstObject] doubleValue]; NSInteger fre = (NSInteger)(60*1000)/(t_c - t_l); if (self.frequency) self.frequency(fre); if ([self.delegate respondsToSelector:@selector(startHeartDelegateRateFrequency:)]) [self.delegate startHeartDelegateRateFrequency:fre]; } else { NSDictionary *point_c = self.points[d_i_c]; NSDictionary *point_r = self.points[d_i_r]; double t_c = [[[point_c allKeys] firstObject] doubleValue]; double t_r = [[[point_r allKeys] firstObject] doubleValue]; NSInteger fre = (NSInteger)(60*1000)/(t_r - t_c); if (self.frequency) self.frequency(fre); if ([self.delegate respondsToSelector:@selector(startHeartDelegateRateFrequency:)]) [self.delegate startHeartDelegateRateFrequency:fre]; } // 4.刪除過期數據 for ( int i = 0; i< 10; i++) { [self.points removeObjectAtIndex:0]; } } } |
我目前是這樣處理的,后面是用的前后兩個峰值與 最低峰值 最接近的那個峰值的時間差,測了幾次又和別的app比較了一下,基本都是正確的,最多也就是上下差1-2次每分鐘。(在數據比較穩定的情況下,如果有更好的方法請推薦,謝謝)
5.畫折線圖 這里用到了 CoreGraphics
PS:首先,使用這個CoreGraphics要在View里面,并且要在View的 drawRect: 方法中使用,不然拿不到畫布。我是為了封裝單獨建立了一個UIView的類。
a.首先還是數據,沒有數據怎么畫
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
@property (strong, nonatomic) NSMutableArray *points; // 在init中初始化數組 self.points = [[NSMutableArray alloc]init]; // 這個可以翻譯過來,也是在init中 self.clearsContextBeforeDrawing = YES; // 外部調用方法 - ( void )drawRateWithPoint:(NSNumber *)point { // 倒敘插入數組 [self.points insertObject:point atIndex:0]; // 刪除溢出屏幕數據 if (self.points.count > self.frame.size.width/6) { [self.points removeLastObject]; } dispatch_async(dispatch_get_main_queue(), ^{ // 這個方法自動調取 drawRect:方法 [self setNeedsDisplay]; }); } |
之前調 setNeedsDisplay ,一直沒有走 drawRect: 方法,或者就直走了一次,然后去百度是說 setNeedsDisplay 會在系統空閑的時候執行 drawRect: ,然后我嘗試著回歸到主線程中調用,就好了。具體原因不是很清楚,也可能是因為要在主線程中修改View。
b.畫折線的方法,具體怎么調整看個人心情了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
CGFloat ww = self.frame.size.width; CGFloat hh = self.frame.size.height; CGFloat pos_x = ww; CGFloat pos_y = hh/2; // 獲取當前畫布 CGContextRef context = UIGraphicsGetCurrentContext(); // 折線寬度 CGContextSetLineWidth(context, 1.0); //消除鋸齒 //CGContextSetAllowsAntialiasing(context,false); // 折線顏色 CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor); CGContextMoveToPoint(context, pos_x, pos_y); for ( int i = 0; i < self.points.count; i++) { float h = [self.points[i] floatValue]; pos_y = hh/2 + (h * hh/2) ; CGContextAddLineToPoint(context, pos_x, pos_y); pos_x -=6; } CGContextStrokePath(context); |
c.為了看起來好看,我還加了網格,當然也是在 drawRect: 中調用的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
static CGFloat grid_w = 30.0f; - ( void )buildGrid { CGFloat wight = self.frame.size.width; CGFloat height = self.frame.size.height; // 獲取當前畫布 CGContextRef context = UIGraphicsGetCurrentContext(); CGFloat pos_x = 0.0f; CGFloat pos_y = 0.0f; // 在wight范圍內畫豎線 while (pos_x < wight) { // 設置網格線寬度 CGContextSetLineWidth(context, 0.2); // 設置網格線顏色 CGContextSetStrokeColorWithColor(context, [UIColor greenColor].CGColor); // 起點 CGContextMoveToPoint(context, pos_x, 1.0f); // 終點 CGContextAddLineToPoint(context, pos_x, height); pos_x +=grid_w; //開始劃線 CGContextStrokePath(context); } // 在height范圍內畫橫線 while (pos_y < height) { CGContextSetLineWidth(context, 0.2); CGContextSetStrokeColorWithColor(context, [UIColor greenColor].CGColor); CGContextMoveToPoint(context, 1.0f, pos_y); CGContextAddLineToPoint(context, wight, pos_y); pos_y +=grid_w; CGContextStrokePath(context); } pos_x = 0.0f; pos_y = 0.0f; // 在wight范圍內畫豎線 while (pos_x < wight) { CGContextSetLineWidth(context, 0.1); CGContextSetStrokeColorWithColor(context, [UIColor greenColor].CGColor); CGContextMoveToPoint(context, pos_x, 1.0f); CGContextAddLineToPoint(context, pos_x, height); pos_x +=grid_w/5; CGContextStrokePath(context); } // 在height范圍內畫橫線 while (pos_y < height) { CGContextSetLineWidth(context, 0.1); CGContextSetStrokeColorWithColor(context, [UIColor greenColor].CGColor); CGContextMoveToPoint(context, 1.0f, pos_y); CGContextAddLineToPoint(context, wight, pos_y); pos_y +=grid_w/5; CGContextStrokePath(context); } } |
總結
寫這個功能的時候,自己有很多思考,也參考了很多其他人的博客、代碼還有別人的畢業論文,呵呵呵,還問了幾個學醫的同學,代碼不難,數據處理的部分可能不太好弄,但是寫完還是有點成就感的。
代碼里還存在很多問題,后期有時間我會慢慢優化,歡迎指正。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。