Vision框架详细解析(十七) —— 基于Vision的轮廓检
版本记录
版本号 | 时间 |
---|---|
V1.0 | 2022.06.01 星期三 |
前言
iOS 11+
和macOS 10.13+
新出了Vision
框架,提供了人脸识别、物体检测、物体跟踪等技术,它是基于Core ML的。可以说是人工智能的一部分,接下来几篇我们就详细的解析一下Vision框架。感兴趣的看下面几篇文章。
1. Vision框架详细解析(一) —— 基本概览(一)
2. Vision框架详细解析(二) —— 基于Vision的人脸识别(一)
3. Vision框架详细解析(三) —— 基于Vision的人脸识别(二)
4. Vision框架详细解析(四) —— 在iOS中使用Vision和Metal进行照片堆叠(一)
5. Vision框架详细解析(五) —— 在iOS中使用Vision和Metal进行照片堆叠(二)
6. Vision框架详细解析(六) —— 基于Vision的显著性分析(一)
7. Vision框架详细解析(七) —— 基于Vision的显著性分析(二)
8. Vision框架详细解析(八) —— 基于Vision的QR扫描(一)
9. Vision框架详细解析(九) —— 基于Vision的QR扫描(二)
10. Vision框架详细解析(十) —— 基于Vision的Body Detect和Hand Pose(一)
11. Vision框架详细解析(十一) —— 基于Vision的Body Detect和Hand Pose(二)
12. Vision框架详细解析(十二) —— 基于Vision的Face Detection新特性(一)
13. Vision框架详细解析(十三) —— 基于Vision的Face Detection新特性(二)
14. Vision框架详细解析(十四) —— 基于Vision的人员分割(一)
15. Vision框架详细解析(十五) —— 基于Vision的人员分割(二)
16. Vision框架详细解析(十六) —— 基于Vision的轮廓检测(一)
源码
1. Swift
首先看下工程组织结构

下面就是源码了
1. CoreGraphicsExtensions.swift
import CoreGraphics
extension CGRect {
var area: Double {
width * height
}
}
2. UserDefaultsExtension.swift
import CoreGraphics
import Foundation
extension UserDefaults {
var minPivot: CGFloat {
get {
if let pivot = object(forKey: Settings.minPivot.rawValue) as? Double {
return pivot
}
return 0.5
}
set {
set(newValue, forKey: Settings.minPivot.rawValue)
}
}
var maxPivot: CGFloat {
get {
if let pivot = object(forKey: Settings.maxPivot.rawValue) as? Double {
return pivot
}
return 0.55
}
set {
set(newValue, forKey: Settings.maxPivot.rawValue)
}
}
var minAdjust: CGFloat {
get {
if let adjust = object(forKey: Settings.minAdjust.rawValue) as? Double {
return adjust
}
return 2.0
}
set {
set(newValue, forKey: Settings.minAdjust.rawValue)
}
}
var maxAdjust: CGFloat {
get {
if let adjust = object(forKey: Settings.maxAdjust.rawValue) as? Double {
return adjust
}
return 2.1
}
set {
set(newValue, forKey: Settings.maxAdjust.rawValue)
}
}
var epsilon: CGFloat {
get {
if let epsilon = object(forKey: Settings.epsilon.rawValue) as? Double {
return epsilon
}
return 0.001
}
set {
set(newValue, forKey: Settings.epsilon.rawValue)
}
}
var iouThresh: CGFloat {
get {
if let thresh = object(forKey: Settings.iouThresh.rawValue) as? Double {
return thresh
}
return 0.85
}
set {
set(newValue, forKey: Settings.iouThresh.rawValue)
}
}
func delete(key: Settings) {
removeObject(forKey: key.rawValue)
}
}
3. VNContourExtension.swift
import Vision
extension VNContour {
var boundingBox: CGRect {
var minX: Float = 1.0
var minY: Float = 1.0
var maxX: Float = 0.0
var maxY: Float = 0.0
for point in normalizedPoints {
if point.x < minX {
minX = point.x
} else if point.x > maxX {
maxX = point.x
}
if point.y < minY {
minY = point.y
} else if point.y > maxY {
maxY = point.y
}
}
return CGRect(
x: Double(minX),
y: Double(minY),
width: Double(maxX - minX),
height: Double(maxY - minY))
}
}
4. Contour.swift
import Vision
struct Contour: Identifiable, Hashable {
let id = UUID()
let area: Double
private let vnContour: VNContour
init(vnContour: VNContour) {
self.vnContour = vnContour
self.area = vnContour.boundingBox.area
}
var normalizedPath: CGPath {
self.vnContour.normalizedPath
}
var aspectRatio: CGFloat {
CGFloat(self.vnContour.aspectRatio)
}
var boundingBox: CGRect {
self.vnContour.boundingBox
}
func intersectionOverUnion(with contour: Contour) -> CGFloat {
let intersection = boundingBox.intersection(contour.boundingBox).area
let union = area + contour.area - intersection
return intersection / union
}
}
5. Settings.swift
import Foundation
enum Settings: String, CaseIterable {
case minPivot
case maxPivot
case minAdjust
case maxAdjust
case epsilon
case iouThresh
}
6. ContentViewModel.swift
import Vision
import UIKit
class ContentViewModel: ObservableObject {
@Published var image: CGImage?
@Published var contours: [Contour] = []
@Published var calculating = false
init() {
let uiImage = UIImage(named: "sample")
let cgImage = uiImage?.cgImage
self.image = cgImage
updateContours()
}
func updateContours() {
calculating = true
Task {
let contours = await asyncUpdateContours()
DispatchQueue.main.async {
self.contours = contours
self.calculating = false
}
}
}
func asyncUpdateContours() async -> [Contour] {
var contours: [Contour] = []
let pivotStride = stride(
from: UserDefaults.standard.minPivot,
to: UserDefaults.standard.maxPivot,
by: 0.1)
let adjustStride = stride(
from: UserDefaults.standard.minAdjust,
to: UserDefaults.standard.maxAdjust,
by: 0.2)
let detector = ContourDetector.shared
detector.set(epsilon: UserDefaults.standard.epsilon)
for pivot in pivotStride {
for adjustment in adjustStride {
detector.set(contrastPivot: pivot)
detector.set(contrastAdjustment: adjustment)
let newContours = (try? detector.process(image: self.image)) ?? []
contours.append(contentsOf: newContours)
}
}
if contours.count < 9000 {
let iouThreshold = UserDefaults.standard.iouThresh
var pos = 0
while pos < contours.count {
let contour = contours[pos]
contours = contours[0...pos] + contours[(pos + 1)...].filter {
contour.intersectionOverUnion(with: $0) < iouThreshold
}
pos += 1
}
}
return contours
}
}
7. ContentView.swift
import SwiftUI
struct ContentView: View {
@StateObject private var model = ContentViewModel()
@State private var showContours = false
@State private var showSettings = false
private var message: String {
if model.calculating {
return "Calculating contours..."
} else {
return "Tap screen to toggle contours"
}
}
var body: some View {
ZStack {
Color.white
if showContours {
ContoursView(contours: model.contours)
} else {
ImageView(image: model.image)
}
VStack {
HStack {
Spacer()
Text(message)
.foregroundColor(.black)
.padding()
Spacer()
}
Spacer()
HStack {
Spacer()
Button(action: {
self.showSettings.toggle()
}, label: {
Image(systemName: "gear")
.padding()
})
}
}
}
.onTapGesture {
self.showContours.toggle()
}
.sheet(isPresented: $showSettings) {
SettingsView()
.onDisappear {
model.updateContours()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
8. ContoursView.swift
import SwiftUI
struct ContoursView: View {
let contours: [Contour]
var body: some View {
GeometryReader { geometry in
ZStack {
ForEach(contours) { contour in
path(for: contour, in: geometry.frame(in: .local))
.stroke(Color.gray, lineWidth: 1)
}
}
}
}
func path(for contour: Contour, in frame: CGRect) -> Path {
let scale = scale(for: contour, in: frame)
let offset = offset(for: scale, in: frame)
return Path(contour.normalizedPath)
.applying(CGAffineTransform(translationX: 0, y: -1.0))
.applying(CGAffineTransform(scaleX: scale, y: -scale))
.applying(CGAffineTransform(translationX: offset.x, y: offset.y))
}
func scale(for contour: Contour, in frame: CGRect) -> CGFloat {
let frameAspect = frame.width / frame.height
if frameAspect > contour.aspectRatio {
return frame.height
} else {
return frame.width
}
}
func offset(for scale: CGFloat, in frame: CGRect) -> CGPoint {
let offsetX = ((frame.width - scale) + frame.minX) / 2.0
let offsetY = ((frame.height - scale) + frame.minY) / 2.0
return CGPoint(x: offsetX, y: offsetY)
}
}
struct ContoursView_Previews: PreviewProvider {
static var previews: some View {
ContoursView(contours: [])
}
}
9. ImageView.swift
import SwiftUI
struct ImageView: View {
let image: CGImage?
private let label = Text("Image to draw")
var body: some View {
if let image = image {
Image(image, scale: 1.0, orientation: .up, label: label)
.resizable()
.scaledToFit()
} else {
EmptyView()
}
}
}
struct ImageView_Previews: PreviewProvider {
static var previews: some View {
ImageView(image: nil)
}
}
10. SettingsView.swift
import SwiftUI
struct SettingsView: View {
@AppStorage(Settings.minPivot.rawValue)
private var minPivot = UserDefaults.standard.minPivot
@AppStorage(Settings.maxPivot.rawValue)
private var maxPivot = UserDefaults.standard.maxPivot
@AppStorage(Settings.minAdjust.rawValue)
private var minAdjust = UserDefaults.standard.minAdjust
@AppStorage(Settings.maxAdjust.rawValue)
private var maxAdjust = UserDefaults.standard.maxAdjust
@AppStorage(Settings.epsilon.rawValue)
private var eps = UserDefaults.standard.epsilon
@AppStorage(Settings.iouThresh.rawValue)
private var iouThresh = UserDefaults.standard.iouThresh
var body: some View {
Form {
Section(header: Text("Vision Request")) {
Text("Min Contrast Pivot: \(String(format: "%.2f", minPivot))")
Slider(value: $minPivot, in: 0.0...1.0, step: 0.05)
Text("Max Contrast Pivot: \(String(format: "%.2f", maxPivot))")
Slider(value: $maxPivot, in: 0.0...1.0, step: 0.05)
Text("Min Contrast Adjustment: \(String(format: "%.1f", minAdjust))")
Slider(value: $minAdjust, in: 0.0...3.0, step: 0.1)
Text("Max Contrast Adjustment: \(String(format: "%.1f", maxAdjust))")
Slider(value: $maxAdjust, in: 0.0...3.0, step: 0.1)
}
Section(header: Text("Simplicity")) {
Text("Polygon Approximation Epsilon: \(String(format: "%.4f", eps))")
Slider(value: $eps, in: 0.0001...0.01, step: 0.0001)
}
Section(header: Text("Filtering")) {
Text("IoU Threshold: \(String(format: "%.3f", iouThresh))")
Slider(value: $iouThresh, in: 0.05...0.95, step: 0.025)
}
}
}
}
struct SettingsView_Previews: PreviewProvider {
static var previews: some View {
SettingsView()
}
}
11. ContourDetector.swift
import Vision
class ContourDetector {
static let shared = ContourDetector()
private var epsilon: Float = 0.001
private lazy var request: VNDetectContoursRequest = {
let req = VNDetectContoursRequest()
return req
}()
private init() {}
private func postProcess(request: VNRequest) -> [Contour] {
guard let results = request.results as? [VNContoursObservation] else {
return []
}
let vnContours = results.flatMap { contour in
(0..<contour.contourCount).compactMap { try? contour.contour(at: $0) }
}
let simplifiedContours = vnContours.compactMap {
try? $0.polygonApproximation(epsilon: self.epsilon)
}
return simplifiedContours.map { Contour(vnContour: $0) }
}
private func perform(_ request: VNRequest, on image: CGImage) throws -> VNRequest {
let requestHandler = VNImageRequestHandler(cgImage: image, options: [:])
try requestHandler.perform([request])
return request
}
func process(image: CGImage?) throws -> [Contour] {
guard let image = image else {
return []
}
let contourRequest = try perform(request, on: image)
return postProcess(request: contourRequest)
}
func set(epsilon: CGFloat) {
self.epsilon = Float(epsilon)
}
func set(contrastPivot: CGFloat?) {
request.contrastPivot = contrastPivot.map { NSNumber(value: $0) }
}
func set(contrastAdjustment: CGFloat) {
request.contrastAdjustment = Float(contrastAdjustment)
}
}
12. AppMain.swift
import SwiftUI
@main
struct AppMain: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
后记
本篇主要讲述了基于
Vision
的轮廓检测,感兴趣的给个赞或者关注~~~
