Core ML简介及实时目标检测,Caffe、Tensorflow与Core ML模型转换、Vision库的使用
转载请注明出处 http://blog.csdn.net/u014205968/article/details/79220659
本篇文章首先会简要介绍iOS 11
推出的Core ML
机器学习框架,接着会以实际的已经训练好的Caffe
、Tensorflow
模型为例,讲解coremltools
转换工具的使用,以及如何在iOS
端运行相关模型。
当今是人工智能元年,随着深度学习的火热,人工智能又一次出现在大众视野中,对于客户端开发人员来说,我们也应当踏入这个领域有所了解,将机器学习与传统App
结合,开发出更“懂”用户的应用。Google
的Tensorflow
早已支持在Android
上运行,苹果在iOS8
推出的Metal
可以用于访问GPU
,使用Metal
就可以实现机器学习的本地化运行,但学习成本太高,在iOS11
中推出的Core ML
使得机器学习本地化运行变得更加方便。
可以预见的是,本地化模型必然是发展趋势,对于实时性较高的应用,如:目标检测、自然场景文本识别与定位、实时翻译等,如果通过网络传输到后台分析,网络延迟就足够让用户放弃这个App
了,比如微信的扫一扫中有翻译的功能,需要通过后台分析结果,所以识别的速度很慢,实用性不强,有道词典完全实现了离线识别和翻译的功能。本地化模型也是对用户隐私的很好保护。
作者水平有限,对于传统机器学习方法了解不多,对于深度学习只在图像识别、目标检测、自然场景文本定位与识别相关领域有所涉猎,所以本文的侧重点在于本地化运行深度学习模型,局限于实时图片识别,本文栗子包括:VGG16
、Resnet
、AlexNet
,以及一些在Caffe Model Zoo
上公开的好玩的模型。对于语音语义相关领域没有研究,因此,本文的栗子均为图像检测、目标识别相关。
本文也不会讲解深度学习的相关内容,作者还没有能力将相关内容讲的很透彻,想要深入到各个模型网络中,直接看论文是最好的选择。
Core ML简介
参考官方文档 CoreML简介
通过Core ML
我们可以将已经训练好的机器学习模型集成到App
中。
一个训练好的模型是指,将机器学习算法应用到一组训练数据集中得出的结果。该模型根据新的输入数据进行结果的预测,举个例子,根据某地区历史房价进行训练的模型,当给定卧室和卫生间的数量就可以预测房子的价格。
Core ML
是特定领域框架和功能的基础。Core ML
支持使用Vision
进行图片分析,Foundation
提供了自然语言处理(比如,使用NSLinguisticTagger类
),GameplayKit
提供了评估学习的决策树,Core ML
本身是建立在上层的低级原语(primitives
),基于如:Accelerate
、BNNS
以及Metal
和Performance Shaders
等。
Core ML
对设备性能进行了优化,优化了内存和功耗。运行在本地设备上既保护了用户的隐私,又可以在没有网络连接时保证应用的功能完整并能够对请求做出响应。
机器学习模型在计算时,计算量往往都很大,单纯依靠CPU
计算很难满足计算需求,通过上图不难发现,整个架构中Accelerate
、BNNS
、Metal
和Performance Shaders
位于最底层,Core ML
直接使用DSP
、GPU
等硬件加速计算和渲染,上层用户不再需要直接操作硬件调用相关C函数,这也是Core ML
进行优化的一部分。在Core ML
之上,提供了Vision库
用于图像分析,Foundation库
提供了自然语言处理的功能,GameplayKit
应该是在游戏中使用的,这些库封装了苹果提供的机器学习模型,提供上层接口供开发者使用。
Vision库
是一个高性能的图像和视频分析库,提供了包括人脸识别、特征检测、场景分类等功能。本文也会举相关栗子。对于自然语言处理和GameplayKit
作者没有涉猎,不做举例。
总而言之,Core ML
提供了高性能的本地化运行机器模型的功能,能够很方便的实现机器学习模型本地化运行,并提供了一些封装好的模型接口供上层使用。其中最重要的当然就是机器学习模型,Core ML
只支持mlmodel
格式的模型,但苹果提供了一个转换工具可以将Caffe
、Keras
等框架训练的机器学习模型转换为mlmodel
格式供应用使用,还有一些第三方的工具可以将Tensorflow
、MXNet
转换为mlmodel
格式的模型,苹果官方也提供了一些mlmodel
格式的深度学习模型,如VGG16
、GooLeNet
等用于ImageNet
物体识别功能的模型,具体可在官网Apple Machine Learning下载。
Core ML实战 - 实时捕获与识别
首先,使用官网提供的模型尝试一下,在上面的网站中,可以下载到物体识别相关的模型有MobileNet
、SqueezeNet
、ResNet50
、Inception V3
、VGG16
,本文以VGG16
为例进行讲解,你也可以下载一个比较小的模型做简单的实验。
将下载的模型mlmodel
文件拖入到XCode
工程中,单击该文件可以看到相关描述,如下图所示:
在这个描述中,Machine Learning Model
下可以看到模型的相关描述信息。模型文件拖入工程以后也会生成模型相关的接口文件,单击Model Class
下的VGG16
即可跳转到VGG16
模型的接口文件中。Model Evaluation Parameters
则提供了模型的输入输出信息,如上图,输入为224*224大小的三通道图片,输出有两个,classLabelProbs
输出每一个分类的置信度字典,该VGG16
可以对1000个类别进行识别,因此字典会有1000个key-value
键值对,classLabel
则输出置信度最高的分类的标签。
单击VGG16
去查看相关接口声明如下:
//VGG16模型的输入类
@interface VGG16Input : NSObject<MLFeatureProvider>
/*
创建输入类,需要传入一个CVPixelBufferRef类型的图像数据
类型需要为kCVPixelFormatType_32BGRA,大小为224*224像素
*/
@property (readwrite, nonatomic) CVPixelBufferRef image;
- (instancetype)init NS_UNAVAILABLE;
//直接使用该构造函数创建对象即可
- (instancetype)initWithImage:(CVPixelBufferRef)image;
@end
//VGG16模型的输出类
@interface VGG16Output : NSObject<MLFeatureProvider>
//前文讲述的1000分类的名称和置信度字典
@property (readwrite, nonatomic) NSDictionary<NSString *, NSNumber *> * classLabelProbs;
//前文讲述的置信度最高的类别名称
@property (readwrite, nonatomic) NSString * classLabel;
//一般不手动创建输出对象,通过模型对象识别时返回结果为输出对象
- (instancetype)init NS_UNAVAILABLE;
//在进行预测是,Core ML会调用该方法创建一个output对象返回给调用者
- (instancetype)initWithClassLabelProbs:(NSDictionary<NSString *, NSNumber *> *)classLabelProbs classLabel:(NSString *)classLabel;
@end
//VGG16模型接口
@interface VGG16 : NSObject
//MLModel即为Core ML抽象的模型对象
@property (readonly, nonatomic, nullable) MLModel * model;
//构造函数,可以直接使用默认构造函数,则默认加载工程文件中名称为VGG16的mlmodel模型文件
//也可以提供远程下载的功能,使用该构造函数来加载模型
- (nullable instancetype)initWithContentsOfURL:(NSURL *)url error:(NSError * _Nullable * _Nullable)error;
/*
进行预测的方法,需要传入VGG16Input对象和一个NSError指针的指针
返回结果为VGG16Ouput对象,从返回的对象中即可获取到识别的结果
*/
- (nullable VGG16Output *)predictionFromFeatures:(VGG16Input *)input error:(NSError * _Nullable * _Nullable)error;
//直接通过CVPixelBufferRef进行预测,省去了创建input对象的步骤
- (nullable VGG16Output *)predictionFromImage:(CVPixelBufferRef)image error:(NSError * _Nullable * _Nullable)error;
@end
这个接口文件中只声明了三个类,VGG16Input
表示模型的输入对象、VGG16Output
表示模型的输出对象、VGG16
表示模型对象,其实对于任何mlmodel
格式的深度学习模型,最终生成的接口文件都是相同的,差别就在于输入输出的不同,所以,掌握了一个模型的使用方法,其他模型都是通用的。
接下来看一下具体的使用代码:
//模型拖入工程后,使用默认构造函数进行模型加载,就会去加载同名的VGG16.mlmodel文件
VGG16 *vgg16Model = [[VGG16 alloc] init];
//加载一个需要识别图片,一定是224*224大小的,否则不能识别
UIImage *image = [UIImage imageNamed:@"test.png"];
//将图片转换为CVPixelBufferRef格式的数据
CVPixelBufferRef imageBuffer = [self pixelBufferFromCGImage:[image CGImage]];
//使用转换后的图片数据构造模型输入对象
VGG16Input *input = [[VGG16Input alloc] initWithImage:imageBuffer];
NSError *error;
//使用VGG16模型进行图片的识别工作
VGG16Output *output = [vgg16Model predictionFromFeatures:input error:&error];
//根据error判断识别是否成功
if (error) {
NSLog(@"Prediction Error %@", error);
} else {
//识别成功,输出可能性最大的分类标签
NSLog(@"Label: %@", output.classLabel);
}
//由于在转换UIImage到CVPixelBufferRef时,手动开辟了一个空间,因此使用完成后需要及时释放
CVPixelBufferRelease(imageBuffer);
//CGImageRef转换CVPiexlBufferRef格式数据
- (CVPixelBufferRef) pixelBufferFromCGImage: (CGImageRef) image {
NSDictionary *options = @{
(NSString*)kCVPixelBufferCGImageCompatibilityKey : @YES,
(NSString*)kCVPixelBufferCGBitmapContextCompatibilityKey : @YES,
};
CVPixelBufferRef pxbuffer = NULL;
/*
注意需要使用kCVPixelFormatType_32BGRA的格式
深度学习常用OpenCV对图片进行处理,OpenCV使用的不是RGBA而是BGRA
这里使用CVPixelBufferCreate手动开辟了一个空间,因此使用完成后一定要释放该空间
*/
CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, CGImageGetWidth(image),
CGImageGetHeight(image), kCVPixelFormatType_32BGRA, (__bridge CFDictionaryRef) options,
&pxbuffer);
if (status != kCVReturnSuccess) {
NSLog(@"Operation failed");
}
NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL);
CVPixelBufferLockBaseAddress(pxbuffer, 0);
void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer);
CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(pxdata, CGImageGetWidth(image),
CGImageGetHeight(image), 8, 4*CGImageGetWidth(image), rgbColorSpace,
kCGImageAlphaNoneSkipFirst);
NSParameterAssert(context);
CGContextConcatCTM(context, CGAffineTransformMakeRotation(0));
CGAffineTransform flipVertical = CGAffineTransformMake( 1, 0, 0, -1, 0, CGImageGetHeight(image) );
CGContextConcatCTM(context, flipVertical);
CGAffineTransform flipHorizontal = CGAffineTransformMake( -1.0, 0.0, 0.0, 1.0, CGImageGetWidth(image), 0.0 );
CGContextConcatCTM(context, flipHorizontal);
CGContextDrawImage(context, CGRectMake(0, 0, CGImageGetWidth(image),
CGImageGetHeight(image)), image);
CGColorSpaceRelease(rgbColorSpace);
CGContextRelease(context);
CVPixelBufferUnlockBaseAddress(pxbuffer, 0);
return pxbuffer;
}
编译运行以后就可以看到输出结果啦,对于目标识别这样的简单问题来说,输入为一张图片,输出为一个分类结果,所有的模型几乎都是这样的处理步骤。首先获取要识别的图片,创建模型对象,创建模型输入对象,通过模型对象进行识别来获取模型输出对象,从输出对象获取结果。
对于官网提供的其他目标识别,Resnet50
、GoogLeNet
等,不再举例了,过程都是一样的,读者可自行实验。
接下来做一点有趣的尝试,通过手机摄像头实时获取拍摄的数据,然后去实时检测目标并给出分类结果。
首先需要做一定的限制,输入图片要求是224*224大小的,通过摄像头获取的图像数据是1080*1920的,如果直接转换为224*224会有拉伸,影响识别结果,所以,作者采用的方法是获取中间区域部分的正方形图像,然后转换为目标大小。
代码如下:
//定义一个深度学习模型执行的block,方便在一个应用内,调用不同的模型
typedef void(^machineLearningExecuteBlock)(CVPixelBufferRef imageBuffer);
//实时目标检测视图类,需要实现一个协议用于获取摄像头的输出数据
@interface RealTimeDetectionViewController() <AVCaptureVideoDataOutputSampleBufferDelegate>
//当前获取到的设备
@property (nonatomic, strong) AVCaptureDevice *device;
/*
设备的输入,表示摄像头或麦克风这样的实际物理硬件
通过AVCaptureDevice对象创建,可以控制实际的物理硬件设备
*/
@property (nonatomic, strong) AVCaptureDeviceInput *input;
//视频输出,还有音频输出等类型
@property (nonatomic, strong) AVCaptureVideoDataOutput *output;
//设备连接,用于将session会话获取的数据与output输出数据,可以同时捕获视频和音频数据
@property (