Apple 在 WWDC 2020(線上版)開發者大會中響起了平地一聲雷,釋出了許多讓人驚喜的新功能,(延伸閱讀:Apple’s own silicon chips for Macs),包括 SwiftUI、ARKit、PencilKit、Create ML 還有 Core ML。但是其中,對我來說最突出的是電腦視覺處理 (computer vision)。
Apple 推出了一系列新 API 之後,Vision 框架得到了更完善的支援。這些新 API 提供了一個更直接的方式,來執行複雜且重要的電腦視覺處理演算法。
從 iOS 14 開始,Vision 框架支援手部及身體的姿勢估計 (Hand and Body Pose Estimation)、光流 (Optical Flow)、軌跡偵測 (Trajectory Detection)、以及輪廓偵測 (Contour Detection)。
現在,先讓我們看看其中一個有趣的新功能 ── 視覺輪廓偵測 (Vision Contour Detection) 吧。
目標
- 了解 Vision 的輪廓偵測請求。
- 在 iOS 14 SwiftUI App 執行一個輪廓偵測請求,來偵測硬幣的輪廓。
- 把圖像傳遞給 Vision 請求之前,先利用 Core Image 濾鏡對圖像進行前處理(pre-process) 來簡化輪廓。我們會嘗試在圖像加上遮罩來降低材質的雜訊,讓輪廓顯得更清楚。
視覺輪廓偵測 (Vision Contour Detection)
輪廓偵測的做法,是偵測圖像邊緣的形狀。實務上來說,它會連接所有相同顏色跟密度的連續點,來組成完整的輪廓。
這個電腦視覺處理功能,非常適用於形狀分析、邊緣偵測,而且如果需要從一張圖像裡面,找到相似形狀物件的情況下,這個功能就非常有用。
硬幣偵測與分割這項處理在 OpenCV 非常常見,現在我們使用 Vision 框架新的 VNDetectContoursRequest
,就可以輕易地在 iOS App 裡執行一樣的功能(不需要第三方套件)。
要處理圖像或畫面,Vision 框架需要將 VNRequest
傳給圖像請求處理器 (image request handler),或連續請求處理器 (sequence request handler),然後我們會得到一個回傳的 VNObservation
類別。
你可以依照執行的請求型別,來決定使用哪個 VNObservation
子類別。在我們這個範例中,我們將使用 VNContoursObservation
來取得所有在圖像中偵測到的輪廓。
我們可以從 VNContoursObservation
查看下列各個參數:
normalizedPath
:它會以標準化座標回傳偵測到的輪廓路徑,我們需要將這個座標轉成 UIKit 的座標,我們在下文將會再說明這一點。contourCount
:這是透過視覺請求 (Vision request) 所偵測到的輪廓數量。topLevelContours
:這是未包含在任何輪廓內的VNContours
陣列 。contour(at:)
:透過這個方法,我們可以傳入其索引值或IndexPath
,來取得一個子輪廓。confidence
:這是整個VNContoursObservation
的信心程度。
請注意:利用
topLevelContours
以及存取子輪廓等方法,可以讓我們很方便地從最後的觀測結果中修改/移除子輪廓。
現在,初步了解了視覺輪廓偵測的請求後,讓我們來看看如何在 iOS 14 的 App 上執行吧!
開始動手
在開始之前,你最少要有 Xcode 12 beta 版本,如此一來,你就可以在 SwiftUI Previews 中直接執行視覺圖像請求 (Vision image request)。
利用 Xcode Wizard 建立一個新的 SwiftUI App,你會看到新的 SwiftUI App
的 Life Cycle:
完成專案建置之後,你會得到下列的程式碼:
@main
struct iOS14VisionContourDetection: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
請注意:從 iOS 14 開始,
SceneDelegate
在 SwiftUI 的App
protocol 已經是過時的用法了,特別是在使用 SwiftUI 為基礎的 App。現在已經改成在struct
的上面加上了@main
註釋,來表明是 App 的進入點。
使用視覺輪廓請求來偵測硬幣
讓我們快速建立一個 SwiftUI 視圖,來執行視覺請求:
struct ContentView: View {
@State var points : String = ""
@State var preProcessImage: UIImage?
@State var contouredImage: UIImage?
var body: some View {
VStack{
Text("Contours: \(points)")
Image("coins")
.resizable()
.scaledToFit()
if let image = preProcessImage{
Image(uiImage: image)
.resizable()
.scaledToFit()
}
if let image = contouredImage{
Image(uiImage: image)
.resizable()
.scaledToFit()
}
Button("Detect Contours", action: {
detectVisionContours()
})
}
}
func detectVisionContours(){
//TODO:
}
}
在上面的程式碼中,我們使用了 SwiftUI 在 iOS 14 才開放的新語法 if let
。讓我們先忽略 preprocessImage
這個狀態物件;現在先來看看 detectVisionContoursss
這個函數,它會依照視覺請求的結果來更新 outputImage
。
func detectVisionContours(){
let context = CIContext()
if let sourceImage = UIImage.init(named: "coin")
{
var inputImage = CIImage.init(cgImage: sourceImage.cgImage!)
let contourRequest = VNDetectContoursRequest.init()
contourRequest.revision = VNDetectContourRequestRevision1
contourRequest.contrastAdjustment = 1.0
contourRequest.detectDarkOnLight = true
contourRequest.maximumImageDimension = 512
let requestHandler = VNImageRequestHandler.init(ciImage: inputImage, options: [:])
try! requestHandler.perform([contourRequest])
let contoursObservation = contourRequest.results?.first as! VNContoursObservation
self.points = String(contoursObservation.contourCount)
self.contouredImage = drawContours(contoursObservation: contoursObservation, sourceImage: sourceImage.cgImage!)
} else {
self.points = "Could not load image"
}
}
從上面的程式碼中,我們已經在 VNDetectContoursRequest
當中設定了 contrastAdjustment
(以加強影像)、以及 detectDarkOnLight
(以在明亮的背景上更好地進行輪廓偵測)這兩個參數。
執行 VNImageRequestsHandler
並且傳入一個圖像後(存在 Assets 資料夾中),我們會得到一個 VNContoursObservation
。
最後,我們在傳入的影像上畫出 normalizedPoints
圖層。
在圖像上畫出輪廓
以下是 drawContours
函數的程式碼:
public func drawContours(contoursObservation: VNContoursObservation, sourceImage: CGImage) -> UIImage {
let size = CGSize(width: sourceImage.width, height: sourceImage.height)
let renderer = UIGraphicsImageRenderer(size: size)
let renderedImage = renderer.image { (context) in
let renderingContext = context.cgContext
let flipVertical = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: size.height)
renderingContext.concatenate(flipVertical)
renderingContext.draw(sourceImage, in: CGRect(x: 0, y: 0, width: size.width, height: size.height))
renderingContext.scaleBy(x: size.width, y: size.height)
renderingContext.setLineWidth(5.0 / CGFloat(size.width))
let redUIColor = UIColor.red
renderingContext.setStrokeColor(redUIColor.cgColor)
renderingContext.addPath(contoursObservation.normalizedPath)
renderingContext.strokePath()
}
return renderedImage
}
上述的程式碼會回傳一個 UIImage
,我們把圖像設定給 contouredImage
這個 SwiftUI 狀態物件之後,畫面就會隨之更新:
如果我們用模擬器來執行是的話,這個結果算是不錯,但是如果我們使用 iOS 14 裝置,有了手機的神經網路引擎 (Neural Engine),結果肯定會更好。
不過,這張圖上對我們而言有太多輪廓了(主要是因為硬幣的材質)。我們可以透過前處理,讓這張圖的輪廓更簡潔(又或是減少這些輪廓)。
使用 Core Image 來為視覺影像請求做前處理
Core Image 是 Apple 一個影像處理及分析的框架。雖然這個框架可以偵測簡單的面部和條形碼,但卻不適用於複雜的電腦視覺處理。
不過,這個框架有超過兩百個圖像濾鏡,在照片 App、或是機器學習模型訓練的資料增強上 (data augmentation) 使用起來也非常方便。
但更重要的是,要為傳給 Vision 框架做分析的圖像做前處理時,Core Image 就大派用場了。
如果你已經看過 WWDC 2020 電腦影像 APIs 的影片,就會看到在演示偵測打孔卡輪廓時,Apple 就利用了 Core Image 的 monochrome 濾鏡為圖像做前處理。
在我們的範例中,要在硬幣上遮罩,使用 monochrome 效果的結果不是很好。特別是對於硬幣這種顏色相似、但又跟背景顏色不同的情況,使用黑白色彩濾鏡會是更好的選擇。
對於上面所使用的前處理型別,我們也設定了高斯濾鏡 (Gaussian filter) 讓圖像更平滑。你可以注意到,使用 monochrome 前處理濾鏡,其實會讓我們得到更多的輪廓。
因此,在進行前處理時,一定要注意需要處理的圖像類型。
經過前處理後,我們把得到的 outputImage
傳給視覺影像請求。GitHub Repository 上有完整的原始碼,你可以在當中找到建立及套用 Core Image 濾鏡的程式碼片段。
分析輪廓
我們可以利用 VNGeometryUtils
類別,來觀察不同的參數,像是輪廓直徑、邊界圓 (Bounding Circle)、面積跟周長、以及長寬比例。我們只需要將要觀察的輪廓物件傳入:
VNGeometryUtils.boundingCircle(for: VNContour)
就可以在一個圖像當中,分辨不同種類的形狀,這開啟了更多新電腦視覺處理的可能性。
更進一步,我們可以在 VNContour
中呼叫 polygonApproximation(withEpsilon:)
方法,將邊緣附近的小雜訊過濾掉來簡化輪廓。
結論
電腦視覺處理在 Apple 未來的混合實境中扮演非常重要的角色,引入了 ARKit 框架的手部和身體姿勢的 API 後,將為建立智慧電腦視覺處理 App 開啟新的可能性。
WWDC 2020 還有很多令人興奮的內容。對於在手機上有機器學習的新可能,我覺得非常興奮。敬請期待之後的更新,謝謝你的閱讀。