教之道 貴以專 昔孟母 擇鄰處 子不學(xué) 斷機(jī)杼
隨著蘋果新品iPhone x
的發(fā)布,正式版iOS 11
也就馬上要推送過(guò)來(lái)了辽旋,在正式版本到來(lái)之前比較好奇协怒,于是就去下載了個(gè)Beat
版本刷了下奕删,感覺(jué)還不錯(cuò)奕坟。WWDC 2017
推出了機(jī)器學(xué)習(xí)框架和ARKit
兩個(gè)比較有意思的東西,本想先來(lái)學(xué)習(xí)學(xué)習(xí)AR
罗侯,無(wú)奈手機(jī)剛好不在版本中.....真受傷器腋,只好來(lái)學(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)單來(lái)說(shuō),就是用大量的數(shù)據(jù)去采集物體的特性特征瓶摆,將其裝入模型凉逛,當(dāng)我們用的時(shí)候,可以通過(guò)查詢模型群井,來(lái)快速區(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í)間∩耘鳎可以這么說(shuō)酬荞,Core ML
讓我們更容易在 App
中使用訓(xùn)練過(guò)的模型,而Vision
讓我們輕松訪問(wèn)蘋果的模型瞧哟,用于面部檢測(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)然這都是蘋果提供的模型,如果你有自己的模型的話斜脂,可以通過(guò)工具將其轉(zhuǎn)換抓艳,參考文檔
在了解上面的模型功能后,我們可以選擇性的對(duì)其進(jìn)行下載帚戳,目前我這里下載了四種模型
將下載好的模型玷或,直接拖入工程中儡首,這里需要注意的問(wèn)題是,需要檢查下
這個(gè)位置是否有該模型偏友,我不知道是不是我這個(gè)
xcode
版本的bug
蔬胯,當(dāng)我拖入的時(shí)候,后面并沒(méi)有位他,這個(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í)候鼻吮,也可以這么來(lái)模仿。在進(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)手了。都說(shuō)心急吃不了熱豆腐后添,果然是這樣笨枯,后面遇到一堆堆坑,容我慢慢道來(lái)。
一開始我的初始化方法是這樣的
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];
這次沒(méi)有崩潰哮塞,而是直接進(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)過(guò)說(shuō)明:
1、模型的后綴為
mlmodelc
2脆丁、調(diào)試的時(shí)候可以取消斷點(diǎn)世舰,方便調(diào)試,省的點(diǎn)來(lái)點(diǎn)去槽卫,當(dāng)然如果想看看內(nèi)部實(shí)現(xiàn)跟压,可以加上斷點(diǎn)
在這里調(diào)通后,就是下一步輸出的問(wèn)題了歼培,上面也看到了有兩個(gè)方法震蒋,一個(gè)是根據(jù)Resnet50Input
一個(gè)是根據(jù)CVPixelBufferRef
,而在Resnet50Input
中又有這么一個(gè)初始化方法
- (instancetype)initWithImage:(CVPixelBufferRef)image;
看來(lái)這個(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é)果,失落的半天不想說(shuō)話无切,幸好有日志荡短,仔細(xì)看日志,你會(huì)發(fā)現(xiàn)哆键,好像是圖片的大小不對(duì)...提示說(shuō)是要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)有這方面的說(shuō)明....
/// 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
到此突然想到塔拳,在上面,我們查看模型的圖中峡竣,也有說(shuō)明靠抑,就是inputs
相關(guān)參數(shù)那列。
到這里澎胡,好像我們還有一個(gè)類沒(méi)有用到孕荠,那就是Vision
,那么通過(guò)Vision
又怎么和Core ML
來(lái)一起實(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
是無(wú)效的戚宦,在modelForMLModel
中个曙,有MLModel
這么一個(gè)對(duì)象的參數(shù),而在Core ML
模型類中受楼,我們也發(fā)現(xiàn)有這么一個(gè)屬性垦搬,看來(lái)我們可以通過(guò)這個(gè)關(guān)系將其聯(lián)系起來(lái)。
@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)什么米绕,是的瑟捣,貌似我們的圖片沒(méi)有關(guān)聯(lián)上來(lái),只好去查找資料栅干,最后發(fā)現(xiàn)一個(gè)最重要的類迈套,那就是VNImageRequestHandler
,在這個(gè)類中碱鳞,我還發(fā)現(xiàn)一個(gè)非常重要的方法
- (BOOL)performRequests:(NSArray<VNRequest *> *)requests error:(NSError **)error;
瞬間就將VNCoreMLRequest
類關(guān)聯(lián)起來(lái)了桑李,因?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);
}
}
通過(guò)這個(gè)方法贵白,我們就可以不用再去考慮圖片的大小了,所有的處理和查詢Vision
已經(jīng)幫我們解決了崩泡。
到這里為止戒洼,還有幾個(gè)疑問(wèn)
- (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í)還沒(méi)去研究,后續(xù)研究好了寥掐,再來(lái)補(bǔ)充靴寂。
下面還是奉上demo,有什么錯(cuò)誤召耘,還望各位多多指教百炬。