教之道 貴以專 昔孟母 擇鄰處 子不學(xué) 斷機(jī)杼
隨著蘋果新品iPhone x的發(fā)布,正式版iOS 11也就馬上要推送過來了,在正式版本到來之前比較好奇,于是就去下載了個(gè)Beat版本刷了下,感覺還不錯(cuò)。WWDC 2017推出了機(jī)器學(xué)習(xí)框架和ARKit兩個(gè)比較有意思的東西,本想先來學(xué)習(xí)學(xué)習(xí)AR,無奈手機(jī)剛好不在版本中.....真受傷,只好來學(xué)習(xí)學(xué)習(xí)機(jī)器學(xué)習(xí)了,下面進(jìn)入正題吧。
先看看大概效果吧

什么是機(jī)器學(xué)習(xí)?
在Core ML出現(xiàn)之前,機(jī)器學(xué)習(xí)應(yīng)該還是比較難學(xué)的,然而這一出現(xiàn),直接大大降低了學(xué)習(xí)的門檻,可見蘋果在這方面花的精力還是不少。那么機(jī)器學(xué)習(xí)到底是什么呢?簡(jiǎn)單來說,就是用大量的數(shù)據(jù)去采集物體的特性特征,將其裝入模型,當(dāng)我們用的時(shí)候,可以通過查詢模型,來快速區(qū)別出當(dāng)前物體屬于什么類,有什么特性等等。而Core ML實(shí)際做的事情就是使用事先訓(xùn)練好的模型,在使用時(shí),對(duì)相關(guān)模塊進(jìn)行預(yù)測(cè),最終返回結(jié)果,這種在本地進(jìn)行預(yù)測(cè)的方式可以不依賴網(wǎng)絡(luò),也可以降低處理時(shí)間。可以這么說,Core ML 讓我們更容易在 App中使用訓(xùn)練過的模型,而Vision讓我們輕松訪問蘋果的模型,用于面部檢測(cè)、面部特征點(diǎn)、文字、矩形、條形碼和物體。
Core ML 和 Vision使用
在使用之前,你必須要保證你的環(huán)境是在xcode 9.0 + iOS 11,然后你可以去官網(wǎng)下載Core ML模型,目前已經(jīng)有6種模型了,分別如下




從其介紹我們可以看出分別的功能
MobileNet:大意是從一組1000個(gè)類別中檢測(cè)出圖像中的占主導(dǎo)地位的物體,如樹、動(dòng)物、食物、車輛、人等等。
SqueezeNet:同上
Places205-GoogLeNet:大意是從205個(gè)類別中檢測(cè)到圖像的場(chǎng)景,如機(jī)場(chǎng)終端、臥室、森林、海岸等。
ResNet50:大意是從一組1000個(gè)類別中檢測(cè)出圖像中的占主導(dǎo)地位的物體,如樹、動(dòng)物、食物、車輛、人等等
Inception v3:同上
VGG16:同上
當(dāng)然這都是蘋果提供的模型,如果你有自己的模型的話,可以通過工具將其轉(zhuǎn)換,參考文檔
在了解上面的模型功能后,我們可以選擇性的對(duì)其進(jìn)行下載,目前我這里下載了四種模型

將下載好的模型,直接拖入工程中,這里需要注意的問題是,需要檢查下

這個(gè)位置是否有該模型,我不知道是不是我這個(gè)
xcode版本的bug,當(dāng)我拖入的時(shí)候,后面并沒有,這個(gè)時(shí)候就需要手動(dòng)進(jìn)行添加一次,在這之后,我們還需要檢查下模型類是否生成,點(diǎn)擊你需要用的模型,然后查看下面位置是否有箭頭

當(dāng)這個(gè)位置箭頭生成好后,我們就可以進(jìn)行代碼的編寫了
代碼部分
在寫代碼之前,我們還需要了解一些東西,那就是模型生成的類中都有什么方法,這里我們就以Resnet50為類,在ViewController中導(dǎo)入頭文件#import "Resnet50.h",當(dāng)我們?cè)谳斎?code>Res的時(shí)候,就會(huì)自動(dòng)補(bǔ)全,導(dǎo)入其它模型的時(shí)候,也可以這么來模仿。在進(jìn)入Resnet50頭文件中,我們可以看到其中分為三個(gè)類,分別為:Resnet50Input、Resnet50Output、Resnet50,看其意思也能猜到,分別為輸入、輸出、和主要使用類。
在Resnet50中,我們可以看到三個(gè)方法,分別如下:
- (nullable instancetype)initWithContentsOfURL:(NSURL *)url error:(NSError * _Nullable * _Nullable)error;
/**
Make a prediction using the standard interface
@param input an instance of Resnet50Input to predict from
@param error If an error occurs, upon return contains an NSError object that describes the problem. If you are not interested in possible errors, pass in NULL.
@return the prediction as Resnet50Output
*/
- (nullable Resnet50Output *)predictionFromFeatures:(Resnet50Input *)input error:(NSError * _Nullable * _Nullable)error;
/**
Make a prediction using the convenience interface
@param image Input image of scene to be classified as color (kCVPixelFormatType_32BGRA) image buffer, 224 pixels wide by 224 pixels high:
@param error If an error occurs, upon return contains an NSError object that describes the problem. If you are not interested in possible errors, pass in NULL.
@return the prediction as Resnet50Output
*/
- (nullable Resnet50Output *)predictionFromImage:(CVPixelBufferRef)image error:(NSError * _Nullable * _Nullable)error;
第一個(gè)應(yīng)該是初始化方法,后面兩個(gè)應(yīng)該是輸出對(duì)象的方法,看到這里,不由的馬上開始動(dòng)手了。都說心急吃不了熱豆腐,果然是這樣,后面遇到一堆堆坑,容我慢慢道來。
一開始我的初始化方法是這樣的
Resnet50* resnet50 = [[Resnet50 alloc] initWithContentsOfURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"Resnet50" ofType:@"mlmodel"]] error:nil];
咋一看,恩,應(yīng)該是相當(dāng)?shù)?code>perfect,然而現(xiàn)實(shí)是殘酷的,出意外的崩潰了...
日志如下
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSURL initFileURLWithPath:]: nil string parameter'
為了找準(zhǔn)位置,我決定打個(gè)全局?jǐn)帱c(diǎn),信心倍增的開始下一次運(yùn)行,然而還是一樣的效果,氣的我,果斷直接只寫了下面的初始化方法
Resnet50* resnet50 = [[Resnet50 alloc] init];
這次沒有崩潰,而是直接進(jìn)入了下面的圖

偶然的機(jī)會(huì),見識(shí)到了
Resnet50內(nèi)部的實(shí)現(xiàn)方法,首先映入眼簾的是mlmodelc這個(gè)類型....想必大家也明白了吧!但是咋就進(jìn)入了這個(gè)地方了?幸運(yùn)的是讓斷點(diǎn)繼續(xù)執(zhí)行兩次就ok了,于是我大膽猜想,是不是斷點(diǎn)引起的,馬上取消斷點(diǎn),重新Run,耶,果然正確,一切順利進(jìn)行中...此時(shí)的我是淚崩的。這一系列經(jīng)過說明:
1、模型的后綴為
mlmodelc2、調(diào)試的時(shí)候可以取消斷點(diǎn),方便調(diào)試,省的點(diǎn)來點(diǎn)去,當(dāng)然如果想看看內(nèi)部實(shí)現(xiàn),可以加上斷點(diǎn)
在這里調(diào)通后,就是下一步輸出的問題了,上面也看到了有兩個(gè)方法,一個(gè)是根據(jù)Resnet50Input一個(gè)是根據(jù)CVPixelBufferRef,而在Resnet50Input中又有這么一個(gè)初始化方法
- (instancetype)initWithImage:(CVPixelBufferRef)image;
看來這個(gè)CVPixelBufferRef是必不可少的了
關(guān)于這個(gè),我在網(wǎng)上找了一個(gè)方法,方法如下
- (CVPixelBufferRef)pixelBufferFromCGImage:(CGImageRef)image{
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], kCVPixelBufferCGImageCompatibilityKey,
[NSNumber numberWithBool:YES], kCVPixelBufferCGBitmapContextCompatibilityKey,
nil];
CVPixelBufferRef pxbuffer = NULL;
CGFloat frameWidth = CGImageGetWidth(image);
CGFloat frameHeight = CGImageGetHeight(image);
CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault,
frameWidth,
frameHeight,
kCVPixelFormatType_32ARGB,
(__bridge CFDictionaryRef) options,
&pxbuffer);
NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL);
CVPixelBufferLockBaseAddress(pxbuffer, 0);
void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer);
NSParameterAssert(pxdata != NULL);
CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(pxdata,
frameWidth,
frameHeight,
8,
CVPixelBufferGetBytesPerRow(pxbuffer),
rgbColorSpace,
(CGBitmapInfo)kCGImageAlphaNoneSkipFirst);
NSParameterAssert(context);
CGContextConcatCTM(context, CGAffineTransformIdentity);
CGContextDrawImage(context, CGRectMake(0,
0,
frameWidth,
frameHeight),
image);
CGColorSpaceRelease(rgbColorSpace);
CGContextRelease(context);
CVPixelBufferUnlockBaseAddress(pxbuffer, 0);
return pxbuffer;
}
在這個(gè)方法寫完之后,我將之前的方法進(jìn)行了完善,得到下面的代碼
- (NSString*)predictionWithResnet50:(CVPixelBufferRef )buffer
{
Resnet50* resnet50 = [[Resnet50 alloc] init];
NSError *predictionError = nil;
Resnet50Output *resnet50Output = [resnet50 predictionFromImage:buffer error:&predictionError];
if (predictionError) {
return predictionError.description;
} else {
return [NSString stringWithFormat:@"識(shí)別結(jié)果:%@,匹配率:%.2f",resnet50Output.classLabel, [[resnet50Output.classLabelProbs valueForKey:resnet50Output.classLabel]floatValue]];
}
}
懷著激動(dòng)的心情,添加了imageview和lable,和下面的代碼
CGImageRef cgImageRef = [imageview.image CGImage];
lable.text = [self predictionWithResnet50:[self pixelBufferFromCGImage:cgImageRef]];
Run...


看到這個(gè)結(jié)果,失落的半天不想說話,幸好有日志,仔細(xì)看日志,你會(huì)發(fā)現(xiàn),好像是圖片的大小不對(duì)...提示說是要224,好吧,那就改改尺寸看看
- (UIImage *)scaleToSize:(CGSize)size image:(UIImage *)image {
UIGraphicsBeginImageContext(size);
[image drawInRect:CGRectMake(0, 0, size.width, size.height)];
UIImage* scaledImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return scaledImage;
}
UIImage *scaledImage = [self scaleToSize:CGSizeMake(224, 224) image:imageview.image];
CGImageRef cgImageRef = [scaledImage CGImage];
lable.text = [self predictionWithResnet50:[self pixelBufferFromCGImage:cgImageRef]];
再Run...

終于成功了!!!,至于結(jié)果嘛,還可以算滿意,畢竟狼王加內(nèi)特就是打籃球的 ,哈哈。
后面我又嘗試了其它類,我原以為尺寸都是224,然而在Inceptionv3的時(shí)候,提示我是要用229,于是我就仔細(xì)查看了下類代碼,發(fā)現(xiàn)其中已經(jīng)有這方面的說明....
/// Input image to be classified as color (kCVPixelFormatType_32BGRA) image buffer, 299 pixels wide by 299 pixels high
/// Input image of scene to be classified as color (kCVPixelFormatType_32BGRA) image buffer, 224 pixels wide by 224 pixels high
到此突然想到,在上面,我們查看模型的圖中,也有說明,就是inputs相關(guān)參數(shù)那列。
到這里,好像我們還有一個(gè)類沒有用到,那就是Vision,那么通過Vision又怎么和Core ML來一起實(shí)現(xiàn)呢?
Vision使用
@interface VNCoreMLModel : NSObject
- (instancetype) init NS_UNAVAILABLE;
/*!
@brief Create a model container to be used with VNCoreMLRequest based on a Core ML model. This can fail if the model is not supported. Examples for a model that is not supported is a model that does not take an image as any of its inputs.
@param model The MLModel from CoreML to be used.
@param error Returns the error code and description, if the model is not supported.
*/
+ (nullable instancetype) modelForMLModel:(MLModel*)model error:(NSError**)error;
@end
在上面VNCoreMLModel類中,我們可以看到其初始化方法之一一個(gè)modelForMLModel,而init是無效的,在modelForMLModel中,有MLModel這么一個(gè)對(duì)象的參數(shù),而在Core ML模型類中,我們也發(fā)現(xiàn)有這么一個(gè)屬性,看來我們可以通過這個(gè)關(guān)系將其聯(lián)系起來。
@interface Resnet50 : NSObject
@property (readonly, nonatomic, nullable) MLModel * model;
在當(dāng)前類繼續(xù)往下翻,就能看到類VNCoreMLRequest
@interface VNCoreMLRequest : VNImageBasedRequest
/*!
@brief The model from CoreML wrapped in a VNCoreMLModel.
*/
@property (readonly, nonatomic, nonnull) VNCoreMLModel *model;
@property (nonatomic)VNImageCropAndScaleOption imageCropAndScaleOption;
/*!
@brief Create a new request with a model.
@param model The VNCoreMLModel to be used.
*/
- (instancetype) initWithModel:(VNCoreMLModel *)model;
/*!
@brief Create a new request with a model.
@param model The VNCoreMLModel to be used.
@param completionHandler The block that is invoked when the request has been performed.
*/
- (instancetype) initWithModel:(VNCoreMLModel *)model completionHandler:(nullable VNRequestCompletionHandler)completionHandler NS_DESIGNATED_INITIALIZER;
- (instancetype) init NS_UNAVAILABLE;
- (instancetype) initWithCompletionHandler:(nullable VNRequestCompletionHandler)completionHandler NS_UNAVAILABLE;
@end
在其中,我們看到方法initWithModel和VNCoreMLModel類相關(guān)聯(lián),于是就有了下面的代碼
- (void)predictionWithResnet50WithImage:(CIImage * )image
{
//兩種初始化方法均可
// Resnet50* resnet50 = [[Resnet50 alloc] initWithContentsOfURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"Resnet50" ofType:@"mlmodelc"]] error:nil];
Resnet50* resnet50 = [[Resnet50 alloc] init];
NSError *error = nil;
//創(chuàng)建VNCoreMLModel
VNCoreMLModel *vnCoreMMModel = [VNCoreMLModel modelForMLModel:resnet50.model error:&error];
// 創(chuàng)建request
VNCoreMLRequest *request = [[VNCoreMLRequest alloc] initWithModel:vnCoreMMModel completionHandler:^(VNRequest * _Nonnull request, NSError * _Nullable error) {
}];
}
到這里,好像還差點(diǎn)什么,是的,貌似我們的圖片沒有關(guān)聯(lián)上來,只好去查找資料,最后發(fā)現(xiàn)一個(gè)最重要的類,那就是VNImageRequestHandler,在這個(gè)類中,我還發(fā)現(xiàn)一個(gè)非常重要的方法
- (BOOL)performRequests:(NSArray<VNRequest *> *)requests error:(NSError **)error;
瞬間就將VNCoreMLRequest類關(guān)聯(lián)起來了,因?yàn)?code>VNCoreMLRequest最終還是繼承VNRequest,在相關(guān)文檔的幫助下,最終有了下面的代碼
- (void)predictionWithResnet50WithImage:(CIImage * )image
{
//兩種初始化方法均可
// Resnet50* resnet50 = [[Resnet50 alloc] initWithContentsOfURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"Resnet50" ofType:@"mlmodelc"]] error:nil];
Resnet50* resnet50 = [[Resnet50 alloc] init];
NSError *error = nil;
//創(chuàng)建VNCoreMLModel
VNCoreMLModel *vnCoreMMModel = [VNCoreMLModel modelForMLModel:resnet50.model error:&error];
// 創(chuàng)建處理requestHandler
VNImageRequestHandler *handler = [[VNImageRequestHandler alloc] initWithCIImage:image options:@{}];
NSLog(@" 打印信息:%@",handler);
// 創(chuàng)建request
VNCoreMLRequest *request = [[VNCoreMLRequest alloc] initWithModel:vnCoreMMModel completionHandler:^(VNRequest * _Nonnull request, NSError * _Nullable error) {
CGFloat confidence = 0.0f;
VNClassificationObservation *tempClassification = nil;
for (VNClassificationObservation *classification in request.results) {
if (classification.confidence > confidence) {
confidence = classification.confidence;
tempClassification = classification;
}
}
self.descriptionLable.text = [NSString stringWithFormat:@"識(shí)別結(jié)果:%@,匹配率:%.2f",tempClassification.identifier,tempClassification.confidence];
}];
// 發(fā)送識(shí)別請(qǐng)求
[handler performRequests:@[request] error:&error];
if (error) {
NSLog(@"%@",error.localizedDescription);
}
}
通過這個(gè)方法,我們就可以不用再去考慮圖片的大小了,所有的處理和查詢Vision已經(jīng)幫我們解決了。
到這里為止,還有幾個(gè)疑問
- (instancetype)initWithCIImage:(CIImage *)image options:(NSDictionary<VNImageOption, id> *)options;
/*!
@brief initWithCIImage:options:orientation creates a VNImageRequestHandler to be used for performing requests against the image passed in as a CIImage.
@param image A CIImage containing the image to be used for performing the requests. The content of the image cannot be modified.
@param orientation The orientation of the image/buffer based on the EXIF specification. For details see kCGImagePropertyOrientation. The value has to be an integer from 1 to 8. This superceeds every other orientation information.
@param options A dictionary with options specifying auxilary information for the buffer/image like VNImageOptionCameraIntrinsics
@note: Request results may not be accurate in simulator due to CI's inability to render certain pixel formats in the simulator
*/
- (instancetype)initWithCIImage:(CIImage *)image orientation:(CGImagePropertyOrientation)orientation options:(NSDictionary<VNImageOption, id> *)options;
就是在VNImageRequestHandler還有許多初始化函數(shù),而且還有些參數(shù),暫時(shí)還沒去研究,后續(xù)研究好了,再來補(bǔ)充。
下面還是奉上demo,有什么錯(cuò)誤,還望各位多多指教。