最近做的项目是医疗相关的, 其中有个功能是开启摄像头和闪光灯, 把手指放在摄像头处,便可以绘画心率曲线, 并估出心跳次数.刚听到这个项目功能点的时候,头很大 毫无头绪,在网上查了查资料 小demo, 最后算是实现了, 但是还是有点bug(线不太稳定, 测得不太准)
一. 实现原理(来自知乎):
用高光(摄像头旁的 LED 闪光灯,或者其他足够亮的光源也可)照亮指尖皮下毛细血管,当心脏将新鲜的血液压入毛细血管时,亮度(红色的深度)会有轻微变化,通过摄像头监测这一有规律变化的间隔,即可算出心跳了(通过摄像头采集的图像红色色调的变化的值,来计算绘图, 算法很难)。
二. 实现代码:
参考了github上一个外国大神的心跳demo
功能主要分为以下几个模块:
1.开启摄像头闪光灯, 展示图层:
需要用到的几个类
AVCaptureDevice :代表抽象的摄像头设备
AVCaptureSession (很重要) :代表着input 和 output 的桥梁, 协调着input到output的数据传输.
AVCaptureInput :代表输入设备(可以是它的子类), 它配置抽象硬件设备的ports
AVCaptureDeviceInput (AVCaptureInput的子类) : 提供一个接口来捕捉AVCaptureDevice的媒体数据
AVCaptureOutput :代表输出数据, 管理着输出的一个视频或者图像
AVCaptureVideoDataOutput (AVCaptureOutput子类) :代表捕捉摄像中压缩或者未压缩的视频帧
AVCaptureConnection :代表一个连接(AVCaptureInputPort端口和AVCaptureOutput端口AVCaptureVideoPreviewLayer 当前session)
AVCaptureVideoDataOutputSampleBufferDelegate (AVCaptureVideoDataOutput 代理) :接收视频捕获缓冲区和示例通知的样本缓冲
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection; :每当AVCaptureVideoDataOutput实例输出一个新视频帧(开启摄像头之后, 摄像头显示图像数据会走这个代理方法)
若想在一个已经使用上的session中(已经startRunning了)做更换新的device、删除旧的device等一系列操作,那么就需要使用如下方法:
[session beginConfiguration];
// Remove an existing capture device.
// Add a new capture device.
// Reset the preset.
[session commitConfiguration];
2.根据显示图层的rgb颜色得到变化值
3.根据变化值,绘图
不说废话直接上代码, 注释还是挺全的,如有不对欢迎校对:
// // MainViewController.h // HeartBeats // // Created by 帝炎魔 on 25/04/15. // Copyright (c) 2015 帝炎魔. All rights reserved. // #import <UIKit/UIKit.h> #import <AVFoundation/AVFoundation.h> @interface MainViewController : UIViewController <AVCaptureVideoDataOutputSampleBufferDelegate> @end
// // MainViewController.m // HeartBeats // // Created by 帝炎魔 on 25/04/15. // Copyright (c) 2015 帝炎魔. All rights reserved. // #import "MainViewController.h" @interface MainViewController () { AVCaptureSession *session; CALayer* imageLayer; NSMutableArray *points; } @end @implementation MainViewController - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self) { // Custom initialization } return self; } - (void)viewDidLoad { [super viewDidLoad]; imageLayer = [CALayer layer]; imageLayer.frame = self.view.layer.bounds; imageLayer.contentsGravity = kCAGravityResizeAspectFill; [self.view.layer addSublayer:imageLayer]; [self setupAVCapture]; } - (void)viewWillDisappear:(BOOL)animated { [self stopAVCapture]; } - (void)setupAVCapture { // Get the default camera device /** * 1. 获取摄像头硬件设备(类型 : Video 摄像类型) */ AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; // 开启摄像头闪光灯 if([device isTorchModeSupported:AVCaptureTorchModeOn]) { [device lockForConfiguration:nil]; device.torchMode=AVCaptureTorchModeOn; [device setTorchMode:AVCaptureTorchModeOn]; // 当关掉摄像头的时候 关闭闪光灯 [device unlockForConfiguration]; } // Create the AVCapture Session // 2. 创建session session = [AVCaptureSession new]; // 开始配置intput output [session beginConfiguration]; // Create a AVCaptureDeviceInput with the camera device // 3. 配置input NSError *error = nil; AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error]; if (error) { UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:[NSString stringWithFormat:@"Error %d", (int)[error code]] message:[error localizedDescription] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alertView show]; //[self teardownAVCapture]; return; } // 给session设置input if ([session canAddInput:deviceInput]) [session addInput:deviceInput]; // AVCaptureVideoDataOutput // 3. 配置output AVCaptureVideoDataOutput *videoDataOutput = [AVCaptureVideoDataOutput new]; NSDictionary *rgbOutputSettings = [NSDictionary dictionaryWithObject: [NSNumber numberWithInt:kCMPixelFormat_32BGRA] forKey:(id)kCVPixelBufferPixelFormatTypeKey]; [videoDataOutput setVideoSettings:rgbOutputSettings]; [videoDataOutput setAlwaysDiscardsLateVideoFrames:YES]; // 开启摄像头采集图像输出的子线程 dispatch_queue_t videoDataOutputQueue = dispatch_queue_create("VideoDataOutputQueue", DISPATCH_QUEUE_SERIAL); // 设置子线程执行代理方法 [videoDataOutput setSampleBufferDelegate:self queue:videoDataOutputQueue]; // session配置output if ([session canAddOutput:videoDataOutput]) [session addOutput:videoDataOutput]; // 4. AVCaptureConnection : 代表一个连接(AVCaptureInputPort端口和AVCaptureOutput端口AVCaptureVideoPreviewLayer 当前session) // AVCaptureVideoPreviewLayer : output输出的展示图层 // 用当前的output 初始化connection AVCaptureConnection* connection = [videoDataOutput connectionWithMediaType:AVMediaTypeVideo]; // 设置最小的视频帧输出间隔 [connection setVideoMinFrameDuration:CMTimeMake(1, 10)]; [connection setVideoOrientation:AVCaptureVideoOrientationPortrait]; // 5.session 配置完成 [session commitConfiguration]; // session <span style="font-size:12px;">开始运行<span style="font-family:宋体;line-height: 150%;">发</span><span style="line-height: 150%;" lang="EN-US">running</span><span style="font-family:宋体;line-height: 150%;">消息给它,它会自动跑起来,把输入设备的东西,提交到输出设备中</span></span> [session startRunning]; } #pragma mark -- 终止session (停止摄像头和闪光灯) - (void)stopAVCapture { [session stopRunning]; session = nil; points = nil; } #pragma mark - AVCaptureVideoDataOutputSampleBufferDelegate /** * 每当AVCaptureVideoDataOutput实例输出一个新视频帧(开启摄像头之后, 摄像头显示图像数据会走这个代理方法) * * @param captureOutput 当前output 对象 * @param sampleBuffer 样本缓冲 对象 * @param connection 捕获连接 对象 */ - (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection { // 获取图层缓冲 CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); CVPixelBufferLockBaseAddress(imageBuffer, 0); void *baseAddress = CVPixelBufferGetBaseAddress(imageBuffer); uint8_t *buf = (uint8_t *)CVPixelBufferGetBaseAddress(imageBuffer); size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer); size_t width = CVPixelBufferGetWidth(imageBuffer); size_t height = CVPixelBufferGetHeight(imageBuffer); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGContextRef context = CGBitmapContextCreate(baseAddress, width, height, 8, bytesPerRow, colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst); float r = 0, g = 0,b = 0; for(int y = 0; y < height; y++) { for(int x = 0; x < width * 4; x += 4) { b += buf[x]; g += buf[x+1]; r += buf[x+2]; } buf += bytesPerRow; } r /= 255 * (float)(width * height); g /= 255 * (float)(width * height); b /= 255 * (float)(width * height); float h,s,v; // 通过算法, 根据rgb值 得到相关的值 RGBtoHSV(r, g, b, &h, &s, &v); static float lastH = 0; float highPassValue = h - lastH; lastH = h; float lastHighPassValue = 0; float lowPassValue = (lastHighPassValue + highPassValue) / 2; lastHighPassValue = highPassValue; // 画心率折现 [self render:context value:[NSNumber numberWithFloat:lowPassValue]]; CGImageRef quartzImage = CGBitmapContextCreateImage(context); CGContextRelease(context); CGColorSpaceRelease(colorSpace); id renderedImage = CFBridgingRelease(quartzImage); // 主线程 把摄像头采集图像放到自定义imageView上 dispatch_async(dispatch_get_main_queue(), ^(void) { [CATransaction setDisableActions:YES]; [CATransaction begin]; imageLayer.contents = renderedImage; [CATransaction commit]; }); } #pragma mark --- 画线方法 - (void)render:(CGContextRef)context value:(NSNumber *)value { if(!points) points = [NSMutableArray new]; [points insertObject:value atIndex:0]; CGRect bounds = imageLayer.bounds; while(points.count > bounds.size.width / 2) [points removeLastObject]; if(points.count == 0) return; CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor); CGContextSetLineWidth(context, 2); CGContextBeginPath(context); CGFloat scale = [[UIScreen mainScreen] scale]; // Flip coordinates from UIKit to Core Graphics CGContextSaveGState(context); CGContextTranslateCTM(context, .0f, bounds.size.height); CGContextScaleCTM(context, scale, scale); float xpos = bounds.size.width * scale; float ypos = [[points objectAtIndex:0] floatValue]; CGContextMoveToPoint(context, xpos, ypos); for(int i = 1; i < points.count; i++) { xpos -= 5; float ypos = [[points objectAtIndex:i] floatValue]; CGContextAddLineToPoint(context, xpos, bounds.size.height / 2 + ypos * bounds.size.height / 2); } CGContextStrokePath(context); CGContextRestoreGState(context); } #pragma mark --- 获取颜色变化的算法 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; } @end