import SwiftUI import AVFoundation import Observation import OnyxDesign /// Führt durch das Anlernen: mehrere Posen, mehrere Aufnahmen je Pose. /// /// **Warum mehrere Posen.** Der Merkmalsvektor ist ein allgemeiner /// Bildmerkmalsvektor und kein Gesichtsmerkmal — er reagiert auf Kopfhaltung /// und Beleuchtung mit. Eine einzige Aufnahme beschreibt genau eine Haltung bei /// genau einem Licht; wer sich danach anders hinsetzt, wird nicht erkannt. @MainActor @Observable public final class FaceEnrollment { public enum Pose: String, CaseIterable, Sendable { case straight, left, right public var localizedName: LocalizedStringKey { switch self { case .straight: "face.pose.straight" case .left: "face.pose.left" case .right: "face.pose.right" } } } /// Wie viele Aufnahmen je Pose. public static let samplesPerPose = 5 public private(set) var pose: Pose = .straight public private(set) var collected = 0 public private(set) var isFinished = false /// Ob gerade ein Gesicht im Bild ist — für die Rückmeldung „bitte ins Bild". public private(set) var seesFace = false @ObservationIgnored private let capture = FaceCapture() @ObservationIgnored private let store = FaceStore() @ObservationIgnored private var templates: [FaceTemplate] = [] /// Erst auf Knopfdruck aufnehmen: sonst sammelt es fünf Bilder derselben /// Sekunde ein, und das ist keine Vielfalt, sondern dasselbe fünfmal. @ObservationIgnored private var isCollecting = false public var failure: String? { capture.failure } /// Für die Vorschau. public var session: AVCaptureSession { capture.session } public init() {} public func start() { capture.onVector = { [weak self] vector in self?.accept(vector) } capture.onNoFace = { [weak self] in self?.seesFace = false } capture.start() } public func stop() { capture.stop() } /// Nimmt die aktuelle Pose auf. public func captureCurrentPose() { isCollecting = true } private func accept(_ vector: [Float]) { seesFace = true guard isCollecting, !isFinished else { return } templates.append(FaceTemplate(pose: pose.rawValue, vector: vector)) collected += 1 guard collected >= Self.samplesPerPose else { return } isCollecting = false collected = 0 guard let next = Pose.allCases.firstIndex(of: pose).map({ $0 + 1 }), next < Pose.allCases.count else { finish() return } pose = Pose.allCases[next] } private func finish() { capture.stop() store.save(templates) isFinished = true } /// Verwirft alles Angelernte. public func reset() { capture.stop() templates = [] collected = 0 pose = .straight isFinished = false store.clear() } } /// Die Kameravorschau. public struct CameraPreview: NSViewRepresentable { private let session: AVCaptureSession public init(session: AVCaptureSession) { self.session = session } public func makeNSView(context: Context) -> NSView { let view = NSView() view.wantsLayer = true let layer = AVCaptureVideoPreviewLayer(session: session) layer.videoGravity = .resizeAspectFill view.layer = layer return view } public func updateNSView(_ view: NSView, context: Context) {} }