iOS-调戏CoreML-这是花瓶?

CoreML 是 Apple 在 WWDC 2017 推出的机器学习框架。但是其到底有什么功能呢,能不能识别花瓶,看看就知道了。

原文发表在个人博客iOS-CoreML-初探,转载请注明出处。

模型

CoreML 中, Apple 定义了一套自己的模型格式,后缀名为: mimodel,通过 CoreML 框架,以及模型库,可以在 App 层面进行机器学习的功能研发。

CoreML

官网已经提供四个模型库供下载。

iOS-CoreML-Model

Demo

官网提供了一个 Demo,要求 XCode 9 + iOS 11 的环境。

下载下来 Run 了一下,不得不说,Apple 对开发者还是非常友好的,直接将模型文件拖到项目中,Xcode 会自动生成接口文件:

import CoreML

class MarsHabitatPricerInput : MLFeatureProvider {
    var solarPanels: Double
    var greenhouses: Double
    var size: Double
    
    var featureNames: Set<String> {
        get {
            return ["solarPanels", "greenhouses", "size"]
        }
    }
    
    func featureValue(for featureName: String) -> MLFeatureValue? {
        if (featureName == "solarPanels") {
            return MLFeatureValue(double: solarPanels)
        }
        if (featureName == "greenhouses") {
            return MLFeatureValue(double: greenhouses)
        }
        if (featureName == "size") {
            return MLFeatureValue(double: size)
        }
        return nil
    }
    
    init(solarPanels: Double, greenhouses: Double, size: Double) {
        self.solarPanels = solarPanels
        self.greenhouses = greenhouses
        self.size = size
    }
}

class MarsHabitatPricerOutput : MLFeatureProvider {
    let price: Double
    
    var featureNames: Set<String> {
        get {
            return ["price"]
        }
    }
    
    func featureValue(for featureName: String) -> MLFeatureValue? {
        if (featureName == "price") {
            return MLFeatureValue(double: price)
        }
        return nil
    }
    
    init(price: Double) {
        self.price = price
    }
}

@objc class MarsHabitatPricer:NSObject {
    var model: MLModel
    init(contentsOf url: URL) throws {
        self.model = try MLModel(contentsOf: url)
    }
    convenience override init() {
        let bundle = Bundle(for: MarsHabitatPricer.self)
        let assetPath = bundle.url(forResource: "MarsHabitatPricer", withExtension:"mlmodelc")
        try! self.init(contentsOf: assetPath!)
    }
    func prediction(input: MarsHabitatPricerInput) throws -> MarsHabitatPricerOutput {
        let outFeatures = try model.prediction(from: input)
        let result = MarsHabitatPricerOutput(price: outFeatures.featureValue(for: "price")!.doubleValue)
        return result
    }
    func prediction(solarPanels: Double, greenhouses: Double, size: Double) throws -> MarsHabitatPricerOutput {
        let input_ = MarsHabitatPricerInput(solarPanels: solarPanels, greenhouses: greenhouses, size: size)
        return try self.prediction(input: input_)
    }
}

可以看到,主要是定义了输入,输出以及预测的格式,调用的时候,也非常简单,传参即可。

但是这些接口文件并没有在 XCode 左边的文件树中出现。

查了一下,是生成在 DerivedData 目录下,估计是想开发者使用起来更简洁。

运行一下,可以看到,主要功能是对价格进行预测。

iOS-CoreML-Demo

貌似稍微有点不够高大上...

Resnet50

官网提供的四个模型库,我们还没用呢,当然要看下能用来干啥,看了一下,貌似主要是物体识别,OK,代码走起。

先下载模型库 Resnet50, 然后创建一个新的 Swift 项目,将其拖进去:

iOS-CoreML-Model-Resnet50

从描述里面可以看出来,其实一个神经网络的分类器,输入是一张像素为 (224 * 224) 的图片,输出为分类结果。

自动生成的接口文件:

import CoreML

class Resnet50Input : MLFeatureProvider {
    var image: CVPixelBuffer
    
    var featureNames: Set<String> {
        get {
            return ["image"]
        }
    }
    
    func featureValue(for featureName: String) -> MLFeatureValue? {
        if (featureName == "image") {
            return MLFeatureValue(pixelBuffer: image)
        }
        return nil
    }
    
    init(image: CVPixelBuffer) {
        self.image = image
    }
}

class Resnet50Output : MLFeatureProvider {
    let classLabelProbs: [String : Double]
    let classLabel: String
    
    var featureNames: Set<String> {
        get {
            return ["classLabelProbs", "classLabel"]
        }
    }
    
    func featureValue(for featureName: String) -> MLFeatureValue? {
        if (featureName == "classLabelProbs") {
            return try! MLFeatureValue(dictionary: classLabelProbs as [NSObject : NSNumber])
        }
        if (featureName == "classLabel") {
            return MLFeatureValue(string: classLabel)
        }
        return nil
    }
    
    init(classLabelProbs: [String : Double], classLabel: String) {
        self.classLabelProbs = classLabelProbs
        self.classLabel = classLabel
    }
}

@objc class Resnet50:NSObject {
    var model: MLModel
    init(contentsOf url: URL) throws {
        self.model = try MLModel(contentsOf: url)
    }
    convenience override init() {
        let bundle = Bundle(for: Resnet50.self)
        let assetPath = bundle.url(forResource: "Resnet50", withExtension:"mlmodelc")
        try! self.init(contentsOf: assetPath!)
    }
    func prediction(input: Resnet50Input) throws -> Resnet50Output {
        let outFeatures = try model.prediction(from: input)
        let result = Resnet50Output(classLabelProbs: outFeatures.featureValue(for: "classLabelProbs")!.dictionaryValue as! [String : Double], classLabel: outFeatures.featureValue(for: "classLabel")!.stringValue)
        return result
    }
    func prediction(image: CVPixelBuffer) throws -> Resnet50Output {
        let input_ = Resnet50Input(image: image)
        return try self.prediction(input: input_)
    }
}

OK,要照片,而且是 CVPixelBuffer 类型的。

但是每次从相册选太烦了,所以我们直接摄像头走起。将 AVCam 的主要功能类复制到项目中。

iOS-CoreML-AVCam

然后,禁用 CameraViewController 中一些不必要的按钮:

self.recordButton.isHidden = true
self.captureModeControl.isHidden = true
self.livePhotoModeButton.isHidden = true
self.depthDataDeliveryButton.isHidden = true

由于,AVCapturePhotoCaptureDelegate 拍照完成的回调为:

 func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) 

看了下 AVCaputrePhoto 的定义,里面刚好有 CVPixelBuffer 格式的属性:

iOS-CoreML-AVCam-PreviewPixelBuffer

直接传进去试试:

// Predicte
if let pixelBuffer = photo.previewPixelBuffer {
    guard let Resnet50CategoryOutput = try? model.prediction(image:pixelBuffer) else {
        fatalError("Unexpected runtime error.")
    }
}

一切看起来很完美,编译通过,运行起来,点一下拍照按钮,额,Crash了,异常:

[core] Error Domain=com.apple.CoreML Code=1 "Input image feature image does not match model description" UserInfo={NSLocalizedDescription=Input image feature image does not match model description, NSUnderlyingError=0x1c0643420 {Error Domain=com.apple.CoreML Code=1 "Image is not valid width 224, instead is 852" UserInfo={NSLocalizedDescription=Image is not valid width 224, instead is 852}}}

哦,忘记改大小了,找到 photoSetting,加上宽高:

if !photoSettings.availablePreviewPhotoPixelFormatTypes.isEmpty {
    photoSettings.previewPhotoFormat = [kCVPixelBufferPixelFormatTypeKey as String: photoSettings.availablePreviewPhotoPixelFormatTypes.first!,
        kCVPixelBufferWidthKey as String : NSNumber(value:224),
        kCVPixelBufferHeightKey as String : NSNumber(value:224)]
}

重新 Run,WTF,Man,居然又报同样的错,好吧,Google 一下,貌似宽高的属性,在 Swift 里面不生效,额。。

没办法,那我们只能将 CVPixelBuffer 先转换成 UIImage,然后改下大小,再转回 CVPixelBuffer,试试:

photoData = photo.fileDataRepresentation()
 
// Change Data to Image
guard let photoData = photoData else {
    return
}
let image = UIImage(data: photoData)
                
// Resize
let newWidth:CGFloat = 224.0
let newHeight:CGFloat = 224.0
UIGraphicsBeginImageContext(CGSize(width:newWidth, height:newHeight))
image?.draw(in:CGRect(x:0, y:0, width:newWidth, height:newHeight))
let newImage = UIGraphicsGetImageFromCurrentImageContext()
 UIGraphicsEndImageContext()
                
guard let finalImage = newImage else {
    return
}
                
// Predicte
guard let Resnet50CategoryOutput = try? model.prediction(image:pixelBufferFromImage(image: finalImage)) else {
    fatalError("Unexpected runtime error.")
}

重新 Run,OK,一切很完美。

最后,为了用户体验,加上摄像头流的暂停和重启,免得在识别的时候,摄像头还一直在动,另外,识别结果通过提醒框弹出来,具体参考文末的源码。

开始玩啦,找支油笔试一下:

iOS-CoreML-Pen

识别成,橡皮擦,好吧,其实是有点像。

再拿小绿植试试:

iOS-CoreML-FlowerPot

花瓶,Are you kidding me ??

其实,效果还是蛮不错的。

刚好下午要去上海 CES Asia,一路拍过去玩,想想都有点小激动。

最后,源码奉上,想玩的同学直接下载编译就行了,别忘了 Star~

看了又看:
深度学习是怎么识别人脸的?
300行代码实现手写汉字识别
如何在一周内做一款拼音输入法
iOS-签名机制和证书原理
iOS-线程同步详解

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,175评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,674评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,151评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,597评论 1 269
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,505评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,969评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,455评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,118评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,227评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,213评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,214评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,928评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,512评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,616评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,848评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,228评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,772评论 2 339

推荐阅读更多精彩内容