美文网首页
Vision框架详细解析(十七) —— 基于Vision的轮廓检

Vision框架详细解析(十七) —— 基于Vision的轮廓检

作者: 刀客传奇 | 来源:发表于2022-06-01 09:31 被阅读0次

版本记录

版本号 时间
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的轮廓检测,感兴趣的给个赞或者关注~~~

相关文章

网友评论

      本文标题:Vision框架详细解析(十七) —— 基于Vision的轮廓检

      本文链接:https://www.haomeiwen.com/subject/nkfxmrtx.html