Core ML第一弹:用Core ML创建一款小的图像识别App

这是一篇译文
原文地址Introduction to Core ML: Building a Simple Image Recognition App

在2017年WWDC上,Apple发行了一系列振奋人心的 frameworks 和 APIs 给我们开发者使用。在所有新的 frameworks 里面最受欢迎的莫过于 Core ML.Core ML 是一款可以用来在你的APP中融入机器学习模型的 framework. Core ML最伟大的地方就是你不需要具备全面的神经网络或者机器学习的知识。而他额外的特性就是你只要转换成 Core ML的模型,就可以使用预先训练过的数据模型。为了演示的目的,我们将使用 Apple 开发者网站提供的 Core ML模型。话不多说,让我们开始学习 Core ML 吧!

Note: 以下课程你需要 Xcode 9 bate 版,而且你需要一台 运行 iOS 11 bate 版系统的设备来测试这个课程中介绍的一些特性。尽管 Xcode 9 bate 同时支持 Swift 3.2 和 4.0,但我们所有的代码都是用 Swift 4.0。

Core ML 是什么

Core ML lets you integrate a broad variety of machine learning model types into your app. In addition to supporting extensive deep learning with over 30 layer types, it also supports standard models such as tree ensembles, SVMs, and generalized linear models. Because it’s built on top of low level technologies like Metal and Accelerate, Core ML seamlessly takes advantage of the CPU and GPU to provide maximum performance and efficiency. You can run machine learning models on the device so data doesn’t need to leave the device to be analyzed.
-- Apple’s official documentation about Core ML

Core ML 在今年的 WWDC 中被宣称是机器学习 framework 中的一个新类型,它将和iOS 11 一起到来。有了 Core ML,你可以将机器学习模型融入你的 App.稍等一下,什么是机器学习?简单来说,机器学习就是一个应用在不需要一个明确的程序的情况下,可以赋予电脑学习的能力。一个机器学习的算法和一些列受训的数据结合在一起的结果就是一个受训过的模型。

trained-model.png

作为一个应用的开发者,我们主要关心的是我们应该怎样应用这些模型到我们的 app,并做一些有意思的事情。非常幸运的是,有了Core ML, Apple 已经把它做到非常简单来让我们把机器学习模型加入我们自己的应用。这给开发者开发一些特性,如图像识别、自然语言处理(NLP)、文本预测等带带来更多的可能性。

现在你可能很想知道,把这种类型的 AI 加入到你的应用是不是很困难。Core ML使用起来非常简单这一点就是它最伟大的一点。在这节课程中,你将会看到仅仅用了10行代码,就可以把 Core ML 用到你的 app 里了。

很酷,对吧?让我们开始吧!

Demo app 预览

我们即将写的这个demo相当简单。我们的应用可以让用户选择是拍摄某样东西或者是直接从相册选择。之后,机器学习算法会尝试去预测图像中的对象是什么。结果可能不完美,但是你可以从中学习到把 Core ML 应用到你的 app 的一些想法。

demo.png

开始写代码

首先,我们用Xcode 9创建一个新的工程。选择 single-view app 作为我们的工程模板,并确保我们的语言选择的是 Swift.

新工程.png

搭建用户界面

作者的建议:如果你想要跳过搭建UI的部分,你可以下载 The starter project,然后直接跳转到 Core ML那一段去.

接下来开始吧!首先到你的Main.stroyboard里面去添加一些 UI 元素到 view 上。选择 View controller, 然后到 Xcode 的菜单栏,点击Editor-> Embed In-> Navigation Controller,之后你就会看见一个导航栏出现在 view 的顶部。给导航栏起一个名字叫 Core ML(或者你自己觉得舒服的名字)。

加入导航控制器.png

之后拖拽两个 bar button item:两个分别在导航栏的两侧。左边的一个,到Attributes Inspector改变System ItemCamera.右边的一个,起名为Library。这另个 button可以让用户选择是从相册选择一张图片还是用相机拍摄一张。

最后你需要加的两个对象是UILabelUIImageView。把UIImageView放在view的正中间,改变它的宽和高为 299 * 299,让它变成一个方形。对于 UILabel,把它放到 view 的下面,并且拉伸它到两边边缘。这就是整个 UI 界面了。

当我还没有谈及怎么去给这些view设置 Auto Layout的时候,我强烈建议你去完成它,避免一些view不能完全显示。如果你不能完成这个,那就在你将要测试的设备上跑一下这个app.

storyboard.png

实现拍照和相册的功能

现在你已经设计好了UI, 让我们转到实现,这一节我们将要实现相册和拍照按钮的功能。在ViewController.swift,首先遵守UINavigationControllerDelegate 协议,这个协议是UIImagePickerController类的 delegate 必须遵守的。

class ViewController: UIViewController, UINavigationControllerDelegate 

之后添加两个 outlet 给 label 和 imageView。 为了方便,我给UIImageView起名 imageView, 给 UILabel起名 classifier. 你到代码看起来长这样:

import UIKit
 
class ViewController: UIViewController, UINavigationControllerDelegate {
    @IBOutlet weak var imageView: UIImageView!
    @IBOutlet weak var classifier: UILabel!
    
     override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

接下来,你需要创建各自的 action 给 bar button item 的点击事件。现在插入下面的 action 方法到 UIViewController类:

@IBAction func camera(_ sender: Any) {
    
    if !UIImagePickerController.isSourceTypeAvailable(.camera) {
        return
    }
    
    let cameraPicker = UIImagePickerController()
    cameraPicker.delegate = self
    cameraPicker.sourceType = .camera
    cameraPicker.allowsEditing = false
    
    present(cameraPicker, animated: true)
}
 
@IBAction func openLibrary(_ sender: Any) {
    let picker = UIImagePickerController()
    picker.allowsEditing = false
    picker.delegate = self
    picker.sourceType = .photoLibrary
    present(picker, animated: true)
}

总结一下我们再各 action 里面所做的事。我们创建了一个UIImagePickerController的常量,之后我们确保用户不能编辑图片(不管是拍照还是从相册选取的)。之后我们设置代理为 self, 最后我们展示UIImagePickerController给用户。

因为我们没有添加UIImagePickerControllerDelegate的方法到ViewController.swift里, 所以我们会出现一个error, 我们将用一个类扩展来遵守这个协议:

extension ViewController: UIImagePickerControllerDelegate {
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        dismiss(animated: true, completion: nil)
    }
}

这一行代码处理了用户点击取消拍照的情况。也实现了UIImagePickerControllerDelegate的代理方法。最终你的代码长这样:

import UIKit
 
class ViewController: UIViewController, UINavigationControllerDelegate {
    
    @IBOutlet weak var imageView: UIImageView!
    @IBOutlet weak var classifier: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }
 
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    @IBAction func camera(_ sender: Any) {
        
        if !UIImagePickerController.isSourceTypeAvailable(.camera) {
            return
        }
        
        let cameraPicker = UIImagePickerController()
        cameraPicker.delegate = self
        cameraPicker.sourceType = .camera
        cameraPicker.allowsEditing = false
        
        present(cameraPicker, animated: true)
    }
    
    @IBAction func openLibrary(_ sender: Any) {
        let picker = UIImagePickerController()
        picker.allowsEditing = false
        picker.delegate = self
        picker.sourceType = .photoLibrary
        present(picker, animated: true)
    }
 
}
 
extension ViewController: UIImagePickerControllerDelegate {
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        dismiss(animated: true, completion: nil)
    }
}

确保你所有的 outlet 变量和 action 方法都和 storyboard 连接了。

要进到拍照和相册,这里还有最后一件事你需要做。到你的Info.plist加入两行:Privacy – Camera Usage DescriptionPrivacy – Photo Library Usage Description。从iOS 10开始,你需要说明你的 app 需要打开相机和相册的原因。

plist-privacy.png

OK! 就是这样了,现在你将要转到我们课程最重要的部分了。再一次提醒,如果你想绕过这些UI的搭建部分,可以下载 The starter project

融入 Core ML 数据模型

现在,让我们切换档位,将Core ML 数据模型融入到我们的 app 里面。 就像我之前提到的,我们需要一个预先训练过的模型来和 Core ML 一起工作。你可以创建你自己的模型,倒是对于这个demo, 我们将使用 Apple 开发者网站提供的 预先训练的模型。

到 Apple 开发者网站的Machine Learning页面,滚动到页面的底部,你可以看到4个预先训练的 Core ML 模型。

pretrained-model.png

在这节课程里面,我们使用Inception v3 模型,但是请随意尝试其他3个模型。一旦你下载了Inception v3 模型,就把它加到你的项目里面,看一下它所展示的内容。

model-desc.png
Note:确保你选择了此工程的Target Membership,否则的话,你将没有权限去到这个文件里面。

在这个界面,你可以看到数据模型的类型是一个自然网络类别。另外一个信息你需要记一下的是这个模型的评估参数。它告诉你这个模型可以接受的输入以及他返回的输出。这里他接收一个 299 * 299的图片,并返回一个最相近的类别,加上每个类别的概率。

另一件你需要关注的是 模型类, 这是从机器学习模型中产生的模型类(Inceptionv3),所以我们可以直接用在我们的代码中。如果你点击Inceptionv3旁边的小箭头,你可以看到这个类的源码。

inceptionv3-class.png

现在,我们把这个模型加到我们的代码里。返回到ViewController.swift,首先,引入 CoreML framework:

import CoreML

之后,声明一个model变量是Inceptionv3类型,并在viewWillAppear()方法中初始化它:

var model: Inceptionv3!
 
override func viewWillAppear(_ animated: Bool) {
    model = Inceptionv3()
}

我知道你可能在想:“为什么我们不更早一点初始化这个 model?” "我们为什么要在viewWillAppear方法中定义它 ?" 。 Well, 亲爱的朋友们,原因就是当你的app尝试去识别图片中的对象的时候,它将会更快一点。

回到Inceptionv3.mlmodel,我们发现这个模型接收的输入就是一个尺寸为299 * 299的图片。因此我们怎么转换这个图片到这个尺寸。这就是接下来我们要做的。

转换图片

ViewController.swift的类扩展里面,像下面一样👇更新代码。我们实现了imagePickerController(_:didFinishPickingMediaWithInfo)方法来处理选中的图片:

extension ViewController: UIImagePickerControllerDelegate {
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        dismiss(animated: true, completion: nil)
    }
    
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
        picker.dismiss(animated: true)
        classifier.text = "Analyzing Image..."
        guard let image = info["UIImagePickerControllerOriginalImage"] as? UIImage else {
            return
        } 
        
        UIGraphicsBeginImageContextWithOptions(CGSize(width: 299, height: 299), true, 2.0)
        image.draw(in: CGRect(x: 0, y: 0, width: 299, height: 299))
        let newImage = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()
        
        let attrs = [kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue, kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue] as CFDictionary
        var pixelBuffer : CVPixelBuffer?
        let status = CVPixelBufferCreate(kCFAllocatorDefault, Int(newImage.size.width), Int(newImage.size.height), kCVPixelFormatType_32ARGB, attrs, &pixelBuffer)
        guard (status == kCVReturnSuccess) else {
            return
        } 
        
        CVPixelBufferLockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0))
        let pixelData = CVPixelBufferGetBaseAddress(pixelBuffer!)
        
        let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
        let context = CGContext(data: pixelData, width: Int(newImage.size.width), height: Int(newImage.size.height), bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer!), space: rgbColorSpace, bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue) //3
        
        context?.translateBy(x: 0, y: newImage.size.height)
        context?.scaleBy(x: 1.0, y: -1.0)
        
        UIGraphicsPushContext(context!)
        newImage.draw(in: CGRect(x: 0, y: 0, width: newImage.size.width, height: newImage.size.height))
        UIGraphicsPopContext()
        CVPixelBufferUnlockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0))
        imageView.image = newImage
    }
}

上面的代码做了以下事情:

  • 1.#7-11行:在这个方法最开始几行,我们从info字典中通过UIImagePickerControllerOriginalImage key拿到选择的图片.当图片选好的时候我们也把UIImagePickerController 不显示。

  • 2.#13-16行:因为我们的模型仅能接收299*299尺寸的图片,所以我们转换图片成为一个方形,然后我们把它赋予另外一个常量newImage

  • 3.#18-23行:现在,我们转换newImage成为一个CVPixelBuffer.如果你不熟悉CVPixelBuffer,从本质上来说它是保存主存储器中像素的图像缓冲器。你可以在这里找到更多关于CVPixelBuffer的内容。

  • 4.#31-32行:我们将图像中存在的所有像素转换成与设备相关的RGB颜色,然后,通过将所有这些数据创建到CGContext中我们可以在需要渲染或更改其中一些基础属性时轻松调用它。这就是我们在转换和缩放图片后两行所做的。

  • 5.#34-38行:最后,我们将图像上下文作为当前上下文,渲染图片,将上下文从顶部栈移除,并设置imageView.imagenewImage.

现在,如果你还是不太明白上面的代码,不用担心,这就是一些高级的 Core Image的代码,他们超出了本节课程的范围。你需要知道的就是我们把拍摄的图片转换成了我们模型所能接收的类型。我会建议你去玩数字并观察结果来更好的理解它。

使用 Core ML

不管怎样,让我们转换注意力到 Core ML,我们使用 Inceptionv3 模型来展示对象识别。有了 Core ML,要完成这个,只需要几行代码。粘贴下列代码到imageView.image = newImage行的下面。

guard let prediction = try? model.prediction(image: pixelBuffer!) else {
    return
}
 
classifier.text = "I think this is a \(prediction.classLabel)."

就是它了!这个Inceptionv3类有一个创建的方法叫prediction(image:),它被用来预测所给图片的对象。我们传给这个方法一个pixelBuffer变量--一个重新调整大小的图片。只要预测的结果返回,我们就会更新classifierlabel 去设置它的文字来展示所识别的内容。

是时候测试一下这个 app 了!编译并运行这个app 到模拟器或者是你的设备(安装了iOS 11 bate系统)。从你的相册选择一张照片或者用相机拍摄一张照片。这个 app 就会告诉你这个图片展示的是什么。

识别成功.jpg

测试的时候,你可能会发现这个app不能准确的识别图片的内容。这不是你代码的原因,而是这个模型的原因。

识别失败.jpg

提要

我希望现在你已经知道了怎么把 Core ML 用到你自己的app. 这只是一篇开场的课程,如果你对转换你训练的Caffe, Keras, or SciKit 模型到 Core ML 模型有兴趣,请继续关注我们Core ML系列的下一个教程,我将会教你怎么把一个模型转换为 Core ML 模型。

demo 地址:complete project on GitHub

更多关于 Core ML framework的细节,你可以查阅官方Core ML 文档.你也可以参考 Apple 在 WWDC 2017上的内容:

你对于Core ML的想法是什么?请留下你的评论并让我知道!

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

推荐阅读更多精彩内容