在 2017 年的 WWDC 中,Apple 釋出了許多新框架(frameworks)都弹,Vision Framework 便是其中一個(gè)哮翘。使用 Vision Framework 掐禁,你不需要高深的知識(shí)就可以很容易地在你的 App 中實(shí)作出電腦視覺技術(shù)(Vision Techniques)!Vision Framework 可以讓你的 App 執(zhí)行許多強(qiáng)大的功能夺姑,例如識(shí)別人臉范圍及臉部特徵(微笑墩邀、皺眉、左眼眉毛等等)盏浙、條碼檢測(cè)眉睹、分類出圖像中的場(chǎng)景荔茬、物件檢測(cè)及追蹤以及視距檢測(cè)。
或許那些已經(jīng)使用 Swift 開發(fā)程序一段時(shí)間的人會(huì)想知道既然已經(jīng)有了Core Image 及 AVFoundation竹海,為什么還要推出 Vision 呢慕蔚?如果我們看一下這張?jiān)?WWDC 演講中出現(xiàn)的表格,我們可以看到 Vision 的準(zhǔn)確度(Accuracy)是最好的斋配,同時(shí)也支持較多的平臺(tái)孔飒。不過 Vision 需要較多的處理時(shí)間以及電源消耗。
圖片來源: Apple’s WWDC video – Vision Framework: Building on Core ML
在本次的教學(xué)中艰争,我們將會(huì)利用 Vision Framework 來作出文字檢測(cè)的功能坏瞄,并實(shí)作出一個(gè)能夠檢測(cè)出文字的 App ,不論字體甩卓、字型及顏色鸠匀。如下圖所示,Vision Framework 可以識(shí)別出印刷及手寫兩種文字逾柿。
編者按:根據(jù)測(cè)試結(jié)果缀棍,Vision Framework 對(duì)中文支持有限。
為了節(jié)省你建置 UI 所花的時(shí)間好專注在學(xué)習(xí) Vision Framework 上机错,你可以下載 Starter Project 作為開始爬范。
請(qǐng)注意你需要 Xcode 9 來完成本次教學(xué),同時(shí)也需要一臺(tái) iOS 11 設(shè)備來測(cè)試弱匪。所有的代碼皆是以 Swift 4 撰寫青瀑。
建立即時(shí)影像
當(dāng)你打開項(xiàng)目時(shí),你可以看到視圖已經(jīng)為你設(shè)定好放在 Storyboard 上了萧诫。接著進(jìn)入 ViewController.swift
狱窘,你會(huì)發(fā)現(xiàn)由一些 outlet 及 function 所構(gòu)成的程序骨架。我們的第一步就是要建立一個(gè)即時(shí)影像來檢測(cè)文字财搁,在 imageView
底下宣告一個(gè) AVCaptureSession
屬性:
var session = AVCaptureSession()
這樣就初始化了一個(gè)可以用來作即時(shí)(real-time)或非即時(shí)(offline)影音獲取的AVCaptureSession
物件。而這個(gè)物件在你要對(duì)即時(shí)影像進(jìn)行操作時(shí)就會(huì)用上躬络。接著尖奔,我們需要把這個(gè) session 連接到我們的設(shè)備上。首先把下面的函數(shù)放入 ViewController.swift
吧穷当。
func startLiveVideo() {
//1
session.sessionPreset = AVCaptureSession.Preset.photo
let captureDevice = AVCaptureDevice.default(for: AVMediaType.video)
//2
let deviceInput = try! AVCaptureDeviceInput(device: captureDevice!)
let deviceOutput = AVCaptureVideoDataOutput()
deviceOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32BGRA)]
deviceOutput.setSampleBufferDelegate(self, queue: DispatchQueue.global(qos: DispatchQoS.QoSClass.default))
session.addInput(deviceInput)
session.addOutput(deviceOutput)
//3
let imageLayer = AVCaptureVideoPreviewLayer(session: session)
imageLayer.frame = imageView.bounds
imageView.layer.addSublayer(imageLayer)
session.startRunning()
}
如果你曾經(jīng)用過 AVFoundation
提茁,你會(huì)發(fā)覺這個(gè)代碼有點(diǎn)熟悉。如果你沒用過馁菜,別擔(dān)心茴扁。我們逐行的將代碼說明一遍。
- 我們首先修改
AVCaptureSession
的設(shè)定汪疮。然后我們?cè)O(shè)定AVMediaType
為影片峭火,因?yàn)槲覀兿M羌磿r(shí)影像毁习,此外它應(yīng)該要一直持續(xù)地運(yùn)作。 - 接著卖丸,我們要定義設(shè)備的輸入及輸出纺且。輸入是指相機(jī)所看到的,而輸出則是指應(yīng)該顯示的影像稍浆。我們希望影像顯示為
kCVPixelFormatType_32BGRA
格式载碌。你可以從這里了解更多關(guān)于像素格式的類型。最后衅枫,我們把輸入及輸出加進(jìn)到AVCaptureSession
嫁艇。 - 最后,我們把含有影像預(yù)覽的 sublayer 加進(jìn)到
imageView
中弦撩,然后讓 session 開始運(yùn)作步咪。
調(diào)用在 viewWillAppear
方法里的這個(gè)函數(shù):
override func viewWillAppear(_ animated: Bool) {
startLiveVideo()
}
因?yàn)樵?viewWillAppear()
中還沒決定 imageView
的范圍,所以覆寫 viewDidLayoutSubviews()
方法來更新圖層的范圍孤钦。
override func viewDidLayoutSubviews() {
imageView.layer.sublayers?[0].frame = imageView.bounds
}
在執(zhí)行之前歧斟,要在 Info.plist
加入一個(gè)條目來說明為何你需要使用到相機(jī)功能。這自 Apple 發(fā)佈 iOS 10 后偏形,都是必須添加的步驟静袖。
現(xiàn)在即時(shí)影像應(yīng)該會(huì)如預(yù)期般的運(yùn)作。然而俊扭,因?yàn)槲覀冞€沒實(shí)作 Vision Framework队橙,所以還沒有文字檢測(cè)功能。而這就是我們接下來要完成的部份萨惑。
實(shí)作文字檢測(cè)
在我們實(shí)作文字檢測(cè)(Text Detection)之前捐康,我們需要了解 Vision Framework 是如何運(yùn)作的∮拱基本上解总,在你的 App 里實(shí)作 Vision 會(huì)有三個(gè)步驟,分別是:
- Requests – Requests 是指當(dāng)你要求 Framework 為你檢測(cè)一些東西時(shí)姐仅。
- Handlers – Handlers 是指當(dāng)你想要 Framework 在 Request 產(chǎn)生后執(zhí)行一些東西或處理這個(gè) Request 時(shí).
- Observations – Observations 是指你想要用你提供的資料做什么花枫。
現(xiàn)在,讓我們從 Request 開始吧掏膏。在初始化的變量 session 底下宣告另一個(gè)變量:
var requests = [VNRequest]()
我們建立了一個(gè)含有一個(gè)通用類別 VNRequest
的陣列劳翰。接著,讓我們?cè)?ViewController
類別里建立一個(gè)函數(shù)來進(jìn)行文字檢測(cè)吧馒疹。
func startTextDetection() {
let textRequest = VNDetectTextRectanglesRequest(completionHandler: self.detectTextHandler)
textRequest.reportCharacterBoxes = true
self.requests = [textRequest]
}
在這個(gè)函數(shù)里佳簸,我們建立一個(gè) VNDetectTextRectanglesRequest
的常數(shù) textRequest
∮北洌基本上它是 VNRequest
的一個(gè)特定型態(tài)生均,只能尋找文字中的矩形听想。當(dāng) Framework 完成了這個(gè) Request,我們希望它調(diào)用 detectTextHandler
函數(shù)疯特。同時(shí)我們也想要知道 Framework 辨識(shí)出了什么哗魂,這也是為什么我們?cè)O(shè)定 reportCharacterBoxes
屬性為 true。最后漓雅,我們?cè)O(shè)定早先建立好的變量requests
為 textRequest
录别。
現(xiàn)在,你應(yīng)該會(huì)得到一些錯(cuò)誤訊息邻吞。這是因?yàn)槲覀冞€沒定義應(yīng)該用來處理 Request 的函數(shù)组题。為了解決這些錯(cuò)誤,建立一個(gè)函數(shù)像:
func detectTextHandler(request: VNRequest, error: Error?) {
guard let observations = request.results else {
print("no result")
return
}
let result = observations.map({$0 as? VNTextObservation})
}
在上面的代碼抱冷,我們首先定義一個(gè)含有所有 VNDetectTextRectanglesRequest
結(jié)果的常數(shù) observations
崔列。接著,我們定義另一個(gè)常數(shù) result
旺遮,它將遍歷所有 Request 的結(jié)果然后轉(zhuǎn)換為 VNTextObservation
型態(tài)赵讯。
現(xiàn)在,更新 viewWillAppear()
方法:
override func viewWillAppear(_ animated: Bool) {
startLiveVideo()
startTextDetection()
}
如果你現(xiàn)在執(zhí)行你的 App耿眉,你不會(huì)看到任何的不同边翼。這是因?yàn)殡m然我們告訴 VNDetectTextRectanglesRequest
要回報(bào)字母方框,但是沒有告訴它該如何回報(bào)鸣剪。這將是我們接下來要完成的部份组底。
繪制方框
在我們的 App 中,我們會(huì)讓 Framework 繪制兩個(gè)方框:一個(gè)所檢測(cè)的每個(gè)字母筐骇,另一個(gè)則是整個(gè)單字债鸡。讓我們就從制作繪制每個(gè)單字的方框開始吧!
func highlightWord(box: VNTextObservation) {
guard let boxes = box.characterBoxes else {
return
}
var maxX: CGFloat = 9999.0
var minX: CGFloat = 0.0
var maxY: CGFloat = 9999.0
var minY: CGFloat = 0.0
for char in boxes {
if char.bottomLeft.x < maxX {
maxX = char.bottomLeft.x
}
if char.bottomRight.x > minX {
minX = char.bottomRight.x
}
if char.bottomRight.y < maxY {
maxY = char.bottomRight.y
}
if char.topRight.y > minY {
minY = char.topRight.y
}
}
let xCord = maxX * imageView.frame.size.width
let yCord = (1 - minY) * imageView.frame.size.height
let width = (minX - maxX) * imageView.frame.size.width
let height = (minY - maxY) * imageView.frame.size.height
let outline = CALayer()
outline.frame = CGRect(x: xCord, y: yCord, width: width, height: height)
outline.borderWidth = 2.0
outline.borderColor = UIColor.red.cgColor
imageView.layer.addSublayer(outline)
}
我們一開始先在函數(shù)里定義一個(gè)常數(shù) boxes
铛纬,他是由 Request 所找到的所有 characterBoxes
的組合厌均。然后,我們定義一些在視圖上的坐標(biāo)點(diǎn)來幫助我們定位方框告唆。最后莫秆,我們建立一個(gè)有給定范圍約束的 CALayer
并將它應(yīng)用在我們的 imageView
上。接下來悔详,就讓我們來為每個(gè)字母建立方框吧。
func highlightLetters(box: VNRectangleObservation) {
let xCord = box.topLeft.x * imageView.frame.size.width
let yCord = (1 - box.topLeft.y) * imageView.frame.size.height
let width = (box.topRight.x - box.bottomLeft.x) * imageView.frame.size.width
let height = (box.topLeft.y - box.bottomLeft.y) * imageView.frame.size.height
let outline = CALayer()
outline.frame = CGRect(x: xCord, y: yCord, width: width, height: height)
outline.borderWidth = 1.0
outline.borderColor = UIColor.blue.cgColor
imageView.layer.addSublayer(outline)
}
跟我們前面所撰寫的代碼相似惹挟,我們使用 VNRectangleObservation
來定義約束條件茄螃,讓我們更容易地勾勒出方框。現(xiàn)在连锯,我們已經(jīng)設(shè)置好所有的函數(shù)了归苍。最后一步便是要連接所有的東西用狱。
連接程序
有兩個(gè)主要的部分需要連接。第一個(gè)是處理 Request 的函數(shù)拼弃。我們先來完成個(gè)這個(gè)吧夏伊。像這樣更新 detectTextHandler
方法:
func detectTextHandler(request: VNRequest, error: Error?) {
guard let observations = request.results else {
print("no result")
return
}
let result = observations.map({$0 as? VNTextObservation})
DispatchQueue.main.async() {
self.imageView.layer.sublayers?.removeSubrange(1...)
for region in result {
guard let rg = region else {
continue
}
self.highlightWord(box: rg)
if let boxes = region?.characterBoxes {
for characterBox in boxes {
self.highlightLetters(box: characterBox)
}
}
}
}
}
我們從讓代碼非同步執(zhí)行開始。首先吻氧,我們移除 imageView
最底層的圖層(如果你有注意到溺忧,我們先前添加了許多圖層到 imageView
中。)接下來盯孙,我們從 VNTextObservation
的結(jié)果里確認(rèn)是否有區(qū)域范圍存在÷成現(xiàn)在,我們調(diào)用沿著范圍(或者說單字)繪制方框的函數(shù)振惰。然后我們確認(rèn)是否有字符方框在這個(gè)范圍里歌溉。如果有,我們調(diào)用方法來沿著字母繪上方框骑晶。
現(xiàn)在痛垛,連接所有東西的最后一個(gè)步驟就是以即時(shí)影像來執(zhí)行我們的 Vision Framework 代碼。我們需要做的是錄制影像并將其轉(zhuǎn)換為 CMSampleBuffer
桶蛔。在 ViewController.swift
的擴(kuò)展(Extension)中插入下面的代碼:
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
return
}
var requestOptions:[VNImageOption : Any] = [:]
if let camData = CMGetAttachment(sampleBuffer, kCMSampleBufferAttachmentKey_CameraIntrinsicMatrix, nil) {
requestOptions = [.cameraIntrinsics:camData]
}
let imageRequestHandler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, orientation: 6, options: requestOptions)
do {
try imageRequestHandler.perform(self.requests)
} catch {
print(error)
}
}
在那邊打住一下匙头。這是我們代碼的最后部分了。這個(gè)擴(kuò)展調(diào)用了 AVCaptureVideoDataOutputSampleBufferDelegate
協(xié)定羽圃∏海基本上這個(gè)函數(shù)所做的就是它確認(rèn) CMSampleBuffer
是否存在以及提供一個(gè) AVCaptureOutput
。接著朽寞,我們建立一個(gè) VNImageOption
型態(tài)的字典(Dictionary)變量 requestOptions
识窿。VNImageOption
是一個(gè)結(jié)構(gòu)(struct)類型,它可以從相機(jī)中保持著資料及屬性脑融。最后我們建立一個(gè) VNImageRequestHandler
物件并執(zhí)行我們?cè)缦冉⒌奈淖?Request喻频。
Build 及 Run 你的 App,看看你得到什么肘迎!
小結(jié)
Well甥温,接下來是個(gè)大工程呢!試著用不同字型妓布、大小姻蚓、字體、粗細(xì)等等來測(cè)試 App 吧匣沼≌玻看看是否你可以擴(kuò)展這個(gè) App 。你可以在下面的回應(yīng)中貼上你如何擴(kuò)展這個(gè)項(xiàng)目。你也可以結(jié)合 Vision Framework 及 Core ML加叁。想要更多關(guān)于 Core ML 的資訊倦沧,可以參閱先前撰寫的 Core ML 介紹教學(xué)。
你可以參考放在 GitHub 上的 完整項(xiàng)目它匕。
更多關(guān)于 Vision Framework 的細(xì)節(jié)可以參考 Vision Framework 官方文件展融。你也可以參考 WWDC 關(guān)于 Vision Framework 的演講:
Vision Framework: Building on Core ML
Advances in Core Image: Filters, Metal, Vision, and More
原文:Using Vision Framework for Text Detection in iOS 11
簡(jiǎn)寶玉寫作群日更打卡第 28 天