iOS Core ML與Vision初識(shí)

教之道 貴以專 昔孟母 擇鄰處 子不學(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)入正題吧。

先看看大概效果吧

coreml1.gif
什么是機(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種模型了,分別如下

1.png
2.png
3.png
4.png

從其介紹我們可以看出分別的功能
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)行下載,目前我這里下載了四種模型

model.png

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

check.png

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

modelOk.png

當(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、Resnet50OutputResnet50,看其意思也能猜到,分別為輸入、輸出、和主要使用類。
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)入了下面的圖

bb.png

偶然的機(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、模型的后綴為mlmodelc
2、調(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)的心情,添加了imageviewlable,和下面的代碼

    CGImageRef cgImageRef = [imageview.image CGImage];
    lable.text = [self predictionWithResnet50:[self pixelBufferFromCGImage:cgImageRef]];

Run...

error1.png
error.png

看到這個(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...

success.png

終于成功了!!!,至于結(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

在其中,我們看到方法initWithModelVNCoreMLModel類相關(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ò)誤,還望各位多多指教。

參考文章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容