Drei neue Module nach dem Bauplan von `ShelfProvider`: reine Regeln getrennt vom Zustand, versionierte Indexdatei, Inhalte in Dateien statt in `UserDefaults` — was dort steht, liest jedes Programm mit `defaults read` mit. Die Zwischenablage achtet `ConcealedType`, `TransientType` und `AutoGeneratedType`. Diese Marken sind eine Verabredung zwischen Programmen und kein Systemdienst; wer sie missachtet, sammelt fremde Passwörter ein. Der Gesichtsvergleich stand im Plan als der eine echte Unsicherheitsfaktor. Ein Spike hat ihn erledigt: `VNGenerateFaceprintRequest` gibt es nicht, aber `VNGenerateImageFeaturePrintRequest` ist öffentlich, liefert 768 Elemente und ein Abstandsmaß — Faktor 3,5 Trennschärfe. Kein eigenes CoreML-Modell nötig. `FaceMatcher` entscheidet über mehrere Posen, mehrere Einzelbilder und eine Sperre nach Fehlversuchen. Die Schwelle liegt bewusst nicht mittig: ein falsches Ja ist ein Fremder am entsperrten Rechner, ein falsches Nein nur ein Passwort, das man ohnehin kennt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
68 lines
2.9 KiB
Swift
68 lines
2.9 KiB
Swift
import Vision
|
|
import AppKit
|
|
|
|
// Gibt Vision ein Merkmal her, mit dem sich ein Gesicht *wiedererkennen* lässt?
|
|
//
|
|
// Öffentlich liefert Vision Rechtecke und Landmarken — beides sagt, wo ein
|
|
// Gesicht ist, nicht wessen. Für den Vergleich braucht es einen Vektor mit
|
|
// Abstandsmaß. Geprüft werden zwei Wege.
|
|
|
|
print("=== Weg 1: VNGenerateFaceprintRequest (privat) ===")
|
|
for name in ["VNGenerateFaceprintRequest", "VNFaceprintObservation",
|
|
"VNFaceObservation", "VNGenerateImageFeaturePrintRequest"] {
|
|
print(" \(name): \(NSClassFromString(name) != nil ? "vorhanden" : "FEHLT")")
|
|
}
|
|
|
|
print("\n=== Weg 2: VNGenerateImageFeaturePrintRequest (öffentlich) ===")
|
|
|
|
/// Ein Testbild: eine Fläche mit einem Muster, damit die Merkmale nicht leer sind.
|
|
func testImage(hue: CGFloat, shift: CGFloat) -> CGImage {
|
|
let size = 224
|
|
let space = CGColorSpaceCreateDeviceRGB()
|
|
let ctx = CGContext(data: nil, width: size, height: size, bitsPerComponent: 8,
|
|
bytesPerRow: 0, space: space,
|
|
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)!
|
|
ctx.setFillColor(NSColor(hue: hue, saturation: 0.6, brightness: 0.9, alpha: 1).cgColor)
|
|
ctx.fill(CGRect(x: 0, y: 0, width: size, height: size))
|
|
ctx.setFillColor(NSColor.black.cgColor)
|
|
for i in 0..<6 {
|
|
ctx.fillEllipse(in: CGRect(x: 20 + CGFloat(i) * 30 + shift, y: 60 + shift,
|
|
width: 24, height: 34))
|
|
}
|
|
return ctx.makeImage()!
|
|
}
|
|
|
|
func featurePrint(_ image: CGImage) -> VNFeaturePrintObservation? {
|
|
let request = VNGenerateImageFeaturePrintRequest()
|
|
let handler = VNImageRequestHandler(cgImage: image, options: [:])
|
|
do {
|
|
try handler.perform([request])
|
|
return request.results?.first as? VNFeaturePrintObservation
|
|
} catch {
|
|
print(" Fehler: \(error.localizedDescription)")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
let a = featurePrint(testImage(hue: 0.1, shift: 0))
|
|
let aAgain = featurePrint(testImage(hue: 0.1, shift: 2)) // fast dasselbe
|
|
let b = featurePrint(testImage(hue: 0.6, shift: 40)) // deutlich anders
|
|
|
|
if let a, let aAgain, let b {
|
|
print(" Merkmalslänge: \(a.elementCount) Elemente, Typ \(a.elementType.rawValue)")
|
|
var near: Float = 0, far: Float = 0
|
|
try? a.computeDistance(&near, to: aAgain)
|
|
try? a.computeDistance(&far, to: b)
|
|
print(" Abstand ähnlich: \(near)")
|
|
print(" Abstand verschieden: \(far)")
|
|
print(" Trennschärfe: \(far > near ? "gegeben" : "NICHT gegeben") (Verhältnis \(far / max(near, 0.0001)))")
|
|
} else {
|
|
print(" Merkmale nicht berechenbar")
|
|
}
|
|
|
|
print("\n=== Gesichtserkennung selbst ===")
|
|
let request = VNDetectFaceLandmarksRequest()
|
|
let handler = VNImageRequestHandler(cgImage: testImage(hue: 0.1, shift: 0), options: [:])
|
|
try? handler.perform([request])
|
|
print(" Landmarken-Anfrage lief durch, \(request.results?.count ?? 0) Gesichter im Testmuster (0 erwartet)")
|