Swift-Vision图像识别框架

背景

最近公司的iOS技术分享知识都多多少少和Vision有点关系,所以打算来学习一下。简单的了解之后,发现Vision是一个功能很强大的框架。

Vision应用场景

  • 人脸检测
  • 图像对比分析
  • 二维码/条形码检测
  • 文字检测
  • 目标跟踪

说到识别检测,除了Vision框架之外,Apple还提供了另外两个框架可以实现:

对此苹果还提供了一份性能对比图:

Vision VS CoreImage VS AVFoundation

从苹果给的对比图可知相对于已有框架Core Image和AVFoundation,Vision的准确度是最好的,同时和Core Image支持平台数量是一样的,但是需要较多的处理时间及电源消耗。Vision又是苹果封装过的库,相较于Core Image这种底层库,API会友好太多,减少了我们的开发量。

Vision体系结构中的重要成员简介

1.RequestHandler

  • VNImageBasedRequest

An object that processes one or more image analysis requests pertaining to a single image.
处理与单个图像有关的一个或多个图像分析请求的对象

  • VNSequenceRequestHandler

An object that processes image analysis requests for each frame in a sequence.
处理序列中每个帧的图像分析请求的对象。

2. VNRequest

  • VNImageBasedRequest

The abstract superclass for image analysis requests that focus on a specific part of an image.
用于图像分析的抽象超类请求关注图像的特定部分。

VNImageBasedRequest.png

3.VNObservation

The abstract superclass for analysis results.
分析结果的抽象超类。

VNObservation.png

Vision的使用流程

1、给我们需求的Request提供相应的RequestHandler
2、RequestHandler需要持有需要识别的图片信息,并将结果分发给每个Request的completionHandler中
3、可以从results属性中得到Observation数组
4、Observation数组中的内容会根据不同的Request返回不同的Observation
5、每个Observation有boundingBox等属性,存储的是识别到的相应特征的坐标
6、我们拿到坐标之后就可以为所欲为了

大致用图片整理表示为:


Vision.png

假如现在有个需求需要对一张图片我们需要做标记出里面的人脸、矩形、二维码、文字官方demo
VNImageRequestHandlerVNSequenceRequestHandler提供的识别方法可传入一个[VNRequest]

//VNImageRequestHandler
public init(cvPixelBuffer pixelBuffer: CVPixelBuffer, options: [VNImageOption : Any] = [:])
public init(cvPixelBuffer pixelBuffer: CVPixelBuffer, orientation: CGImagePropertyOrientation, options: [VNImageOption : Any] = [:])
public init(cgImage image: CGImage, options: [VNImageOption : Any] = [:])
public init(cgImage image: CGImage, orientation: CGImagePropertyOrientation, options: [VNImageOption : Any] = [:])
public init(ciImage image: CIImage, options: [VNImageOption : Any] = [:])
public init(ciImage image: CIImage, orientation: CGImagePropertyOrientation, options: [VNImageOption : Any] = [:])
public init(url imageURL: URL, options: [VNImageOption : Any] = [:])
public init(url imageURL: URL, orientation: CGImagePropertyOrientation, options: [VNImageOption : Any] = [:])
public init(data imageData: Data, options: [VNImageOption : Any] = [:])
public init(data imageData: Data, orientation: CGImagePropertyOrientation, options: [VNImageOption : Any] = [:])
open func perform(_ requests: [VNRequest]) throws
//VNSequenceRequestHandler
open func perform(_ requests: [VNRequest], on pixelBuffer: CVPixelBuffer) throws
open func perform(_ requests: [VNRequest], on pixelBuffer: CVPixelBuffer, orientation: CGImagePropertyOrientation) throws
open func perform(_ requests: [VNRequest], on image: CGImage) throws
open func perform(_ requests: [VNRequest], on image: CGImage, orientation: CGImagePropertyOrientation) throws
open func perform(_ requests: [VNRequest], on image: CIImage) throws
open func perform(_ requests: [VNRequest], on image: CIImage, orientation: CGImagePropertyOrientation) throws
open func perform(_ requests: [VNRequest], onImageURL imageURL: URL) throws
open func perform(_ requests: [VNRequest], onImageURL imageURL: URL, orientation: CGImagePropertyOrientation) throws
open func perform(_ requests: [VNRequest], onImageData imageData: Data) throws
open func perform(_ requests: [VNRequest], onImageData imageData: Data, orientation: CGImagePropertyOrientation) throws

所以我们在查询Vision之前创建所有请求,将他们捆绑在请求数组中,并在一次调用中提交该数组.Vision运行每个请求并在其自己的线程上执行其完成处理程序。CoreImage的话需要初始化4个CIDetector分别将识别类型设置为:CIDetectorTypeFace、CIDetectorTypeRectangle、CIDetectorTypeQRCode、CIDetectorTypeText.相当于对一张图片会进行4次计算.比较浪费资源.

从上面的也可以看出Vision的识别具有方向性.如果识别的方法和图片的方向不一致,Vision可能就无法正确检测出我们想要的特征.

另外查看以上两个类的初始化方法,可知Vision支持的初始化图片数据类型:

  • CVPixelBuffer
  • CGImage
  • CIImage
  • URL
  • Data

关于人脸检测,CoreImage可识别的有:
详情可看

open class CIFaceFeature : CIFeature {
    open var bounds: CGRect { get }
    open var hasLeftEyePosition: Bool { get }
    open var leftEyePosition: CGPoint { get }
    open var hasRightEyePosition: Bool { get }
    open var rightEyePosition: CGPoint { get }
    open var hasMouthPosition: Bool { get }
    open var mouthPosition: CGPoint { get }
    open var hasTrackingID: Bool { get }
    open var trackingID: Int32 { get }
    open var hasTrackingFrameCount: Bool { get }
    open var trackingFrameCount: Int32 { get }
    open var hasFaceAngle: Bool { get }
    open var faceAngle: Float { get }
    open var hasSmile: Bool { get }
    open var leftEyeClosed: Bool { get }
    open var rightEyeClosed: Bool { get }
}

Vision可识别的有:

open var boundingBox: CGRect { get }
open var landmarks: VNFaceLandmarks2D? { get }
open var roll: NSNumber? { get }
open var yaw: NSNumber? { get }
open class VNFaceLandmarks2D : VNFaceLandmarks {
    open var allPoints: VNFaceLandmarkRegion2D? { get }
    open var faceContour: VNFaceLandmarkRegion2D? { get }//从左脸颊到下巴到右脸颊的面部轮廓的点的区域
    open var leftEye: VNFaceLandmarkRegion2D? { get }//左眼轮廓
    open var rightEye: VNFaceLandmarkRegion2D? { get }//右眼轮廓
    open var leftEyebrow: VNFaceLandmarkRegion2D? { get }//左边眉毛的轮廓
    open var rightEyebrow: VNFaceLandmarkRegion2D? { get }//右边眉毛的轮廓
    open var nose: VNFaceLandmarkRegion2D? { get }//鼻子的轮廓
    open var noseCrest: VNFaceLandmarkRegion2D? { get }//鼻子中央嵴痕迹的点的区域。
    open var medianLine: VNFaceLandmarkRegion2D? { get }//脸中心线轨迹的点的区域
    open var outerLips: VNFaceLandmarkRegion2D? { get }//外嘴唇的轮廓
    open var innerLips: VNFaceLandmarkRegion2D? { get }//内嘴唇的轮廓
    open var leftPupil: VNFaceLandmarkRegion2D? { get }//左边瞳孔的轮廓
    open var rightPupil: VNFaceLandmarkRegion2D? { get }//右边瞳孔的轮廓
}
open class VNFaceLandmarkRegion2D : VNFaceLandmarkRegion {
    open var __normalizedPoints: UnsafePointer<CGPoint> { get } //某一部位所有的像素点
    open func __pointsInImage(imageSize: CGSize) -> UnsafePointer<CGPoint>//某一部位的所有像素点的个数
}

举例:
识别图片,并绘制一个矩形去标示我们的人脸.

lazy var faceDetectionRequest = VNDetectFaceRectanglesRequest(completionHandler: self.handleDetectedFaces)
fileprivate func handleDetectedFaces(request: VNRequest?, error: Error?) {
        if let nsError = error as NSError? {
            self.presentAlert("Face Detection Error", error: nsError)
            return
        }
        // Perform drawing on the main thread.
        DispatchQueue.main.async {
            guard let drawLayer = self.pathLayer,
                let results = request?.results as? [VNFaceObservation] else {
                    return
            }
            self.draw(faces: results, onImageWithBounds: drawLayer.bounds)
            drawLayer.setNeedsDisplay()
        }
    }

识别图片,并绘制曲线去标示我们的人脸特征(脸的轮廓、左右眼、左右眉毛……)

lazy var faceLandmarkRequest = VNDetectFaceLandmarksRequest(completionHandler: self.handleDetectedFaceLandmarks)
fileprivate func handleDetectedFaceLandmarks(request: VNRequest?, error: Error?) {
        if let nsError = error as NSError? {
            self.presentAlert("Face Landmark Detection Error", error: nsError)
            return
        }
        // Perform drawing on the main thread.
        DispatchQueue.main.async {
            guard let drawLayer = self.pathLayer,
                let results = request?.results as? [VNFaceObservation] else {
                    return
            }
            self.drawFeatures(onFaces: results, onImageWithBounds: drawLayer.bounds)
            drawLayer.setNeedsDisplay()
        }
    }
此时此刻此景,上头姐妹应该附图一张:biu~~

上头姐妹.jpg

从上图可看出图片中的人脸轮廓以及下方的文字都被识别出来了.

识别文字和二维码和这个类似就不贴了,只是需要注意的是:
1、对于文本观察,通过检查属性来定位单个字符。characterBoxes

// Tell Vision to report bounding box around each character.
textDetectRequest.reportCharacterBoxes = true

2、对于条形码观察,symbologies包含属性中的有效负载信息

// Restrict detection to most common symbologies.
barcodeDetectRequest.symbologies = [.QR, .Aztec, .UPCE]

3、对于矩形观察,通过设置一些属性可以起到过滤检测结果的需求:

 // Customize & configure the request to detect only certain rectangles.
rectDetectRequest.maximumObservations = 8 // Vision currently supports up to 16.
rectDetectRequest.minimumConfidence = 0.6 // Be confident.
rectDetectRequest.minimumAspectRatio = 0.3 // height / width

4、对于地平线角度的观察,demo里面没有,但是也比较容易理解.

lazy var horizonRequest = VNDetectHorizonRequest(completionHandler: self.handleDetectedHorizon)
fileprivate func handleDetectedHorizon(request: VNRequest?, error: Error?) {
        if let nsError = error as NSError? {
            self.presentAlert("Horizon Detection Error", error: nsError)
            return
        }

        guard let results = request?.results as? [VNHorizonObservation] else {
            return
        }
        results.forEach({ observation in
            print(observation.angle)//观察到的地平线的角度。
            print(observation.transform)//变换应用于检测到的地平线。
        })
    }

在创建了所有的请求之后,我们将其加入数组:

 /// - Tag: CreateRequests
    fileprivate func createVisionRequests() -> [VNRequest] {
        
        // Create an array to collect all desired requests.
        var requests: [VNRequest] = []
        
        // Create & include a request if and only if switch is ON.
        if self.rectSwitch.isOn {
            requests.append(self.rectangleDetectionRequest)
        }
        if self.faceSwitch.isOn {
            // Break rectangle & face landmark detection into 2 stages to have more fluid feedback in UI.
            requests.append(self.faceDetectionRequest)
            requests.append(self.faceLandmarkRequest)
        }
        if self.textSwitch.isOn {
            requests.append(self.textDetectionRequest)
        }
        if self.barcodeSwitch.isOn {
            requests.append(self.barcodeDetectionRequest)
        }

        requests.append(self.horizonRequest)
        // Return grouped requests as a single array.
        return requests
    }

然后再调用perform去执行识别.需要注意的是,识别的图片的方法要和我们orientation的方向一致(demo代码中有做处理).其次因为识别的过程比较消耗资源,因此使用后台队列以避免在执行时阻塞主队列。
然后回到上面我们写的completionHandler,我们拿到VNObservation之后会在主线程去刷新绘制我们的UI.

/// - Tag: PerformRequests
    fileprivate func performVisionRequest(image: CGImage, orientation: CGImagePropertyOrientation) {
        
        // Fetch desired requests based on switch status.
        let requests = createVisionRequests()
        // Create a request handler.
        let imageRequestHandler = VNImageRequestHandler(cgImage: image,
                                                        orientation: orientation,
                                                        options: [:])
        
        // Send the requests to the request handler.
        DispatchQueue.global(qos: .userInitiated).async {
            do {
                try imageRequestHandler.perform(requests)
            } catch let error as NSError {
                print("Failed to perform image request: \(error)")
                self.presentAlert("Image Request Failed", error: error)
                return
            }
        }
    }

上面的资料以及demo都是描述检测静止图像中的对象,vision还有个重要的功能就是可以实时去检测对象,比如:
实时跟踪用户脸部demo链接
大致实现过程:
1、配置相机捕获视频
2、识别视频中的人脸并检测出相应的特征
有一点需要注意的是,VNImageRequestHandler可以检测静态的对象,但是他不能从一帧到下一帧去携带信息,所以对于实时的跟踪我们需要用到VNSequenceRequestHandler.对于request也是需要用到VNTrackObjectRequest

open class VNTrackObjectRequest : VNTrackingRequest {
    //使用检测到的对象观察创建新的对象跟踪请求。
    public init(detectedObjectObservation observation: VNDetectedObjectObservation)
    public init(detectedObjectObservation observation: VNDetectedObjectObservation, completionHandler: VNRequestCompletionHandler? = nil)
}

整个环节,比较复杂的部分就是捕获视频之后的识别处理,我们在通过摄像头捕获视频之后,拿到sampleBuffer,如果检测器没有检测到面部则创建VNImageRequestHandler请求检测面部,一旦检测面部成功,则通过创建VNTrackObjectRequest去跟踪检测它.
核心代码如下:

guard let requests = self.trackingRequests, !requests.isEmpty else {
            //如果检测器没有检测到面部,创建VNImageRequestHandler请求检测面部
            let imageRequestHandler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer,
                                                            orientation: exifOrientation,
                                                            options: requestHandlerOptions)
            
            do {
                guard let detectRequests = self.detectionRequests else {
                    return
                }
                try imageRequestHandler.perform(detectRequests)
            } catch let error as NSError {
                NSLog("Failed to perform FaceRectangleRequest: %@", error)
            }
            return
        }
        //一旦检测面部成功,则通过创建VNTrackObjectRequest去跟踪检测它
        do {
            try self.sequenceRequestHandler.perform(requests,
                                                     on: pixelBuffer,
                                                     orientation: exifOrientation)
        } catch let error as NSError {
            NSLog("Failed to perform SequenceRequest: %@", error)
        }
        
        var newTrackingRequests = [VNTrackObjectRequest]()
        //...(此处省略一抹多的代码)
        do {
            try imageRequestHandler.perform(faceLandmarkRequests)
        } catch let error as NSError {
            NSLog("Failed to perform FaceLandmarkRequest: %@", error)
        }

实时跟踪整个视频中的对象demo链接

除此之外,Vision还可以和CoreML结合进行分类请求和标记图像等,总之是一个值得学习的框架.
有兴趣的小伙伴可自行去下载demo来把玩一下.

另外就是和Vision框架看起来会误会的还有一个框架VisionKit:使用iOS相机扫描在Notes应用程序中捕获的文档。目前还是beta版本.

学习资料如下:
Swift之Vision 图像识别框架
iOS黑科技之(AVFoundation)动态人脸识别(二)
基于iOS8以上版本的AV Foundation框架特性之--AVCaptureDevice

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

推荐阅读更多精彩内容