Section 4/151 menit
4. Vision Framework — Analisis Gambar dan Video
4. Vision Framework — Analisis Gambar dan Video
Setup dan Import
swift
// Wajib import
import Vision
import CoreImage
import UIKit
Text Recognition (OCR)
swift
// Vision: OCR dengan VNRecognizeTextRequest
@MainActor
class OCRProcessor {
func recognizeText(
in image: UIImage,
languages: [String] = ["en-US", "id-ID"]
) async throws -> [RecognizedTextBlock] {
guard let cgImage = image.cgImage else {
throw VisionError.invalidImage
}
return try await withCheckedThrowingContinuation { continuation in
let request = VNRecognizeTextRequest { request, error in
if let error {
continuation.resume(throwing: error)
return
}
let blocks = request.results?
.compactMap { result -> RecognizedTextBlock? in
guard let candidate = result.topCandidates(1).first else { return nil }
return RecognizedTextBlock(
text: candidate.string,
confidence: candidate.confidence,
boundingBox: result.boundingBox // normalized 0.0-1.0
)
} ?? []
continuation.resume(returning: blocks)
}
request.recognitionLevel = .accurate // atau .fast
request.usesLanguageCorrection = true
request.recognitionLanguages = languages
request.automaticallyDetectsLanguage = true // iOS 16+
let handler = VNImageRequestHandler(
cgImage: cgImage,
orientation: .up
)
do {
try handler.perform([request])
} catch {
continuation.resume(throwing: error)
}
}
}
}
struct RecognizedTextBlock {
let text: String
let confidence: Float
let boundingBox: CGRect // normalized coordinates
}
Face Detection dan Landmarks
swift
// Vision: deteksi wajah dan titik landmark
class FaceAnalyzer {
struct FaceAnalysis {
let boundingBox: CGRect
let landmarks: VNFaceLandmarks2D?
let roll: Double?
let yaw: Double?
let pitch: Double? // iOS 16+
let isBlinking: Bool
let hasSmile: Bool
}
func analyzeFaces(in image: UIImage) async throws -> [FaceAnalysis] {
guard let cgImage = image.cgImage else { throw VisionError.invalidImage }
return try await withCheckedThrowingContinuation { continuation in
// Request landmarks (includes face detection)
let landmarkRequest = VNDetectFaceLandmarksRequest { request, error in
if let error { continuation.resume(throwing: error); return }
let analyses = request.results?.compactMap { observation -> FaceAnalysis? in
let face = observation as? VNFaceObservation
guard let face else { return nil }
// Roll/yaw untuk pose estimation
let roll = face.roll?.doubleValue
let yaw = face.yaw?.doubleValue
let pitch = face.pitch?.doubleValue // iOS 16+
// Detect blink dari landmark eye open
let leftEye = face.landmarks?.leftEye
let rightEye = face.landmarks?.rightEye
let isBlinking = (leftEye?.pointsInImage(imageSize: .zero).isEmpty ?? true)
return FaceAnalysis(
boundingBox: face.boundingBox,
landmarks: face.landmarks,
roll: roll,
yaw: yaw,
pitch: pitch,
isBlinking: isBlinking,
hasSmile: false // perlu custom ML model untuk smile detection
)
} ?? []
continuation.resume(returning: analyses)
}
let handler = VNImageRequestHandler(cgImage: cgImage)
try? handler.perform([landmarkRequest])
}
}
// Convert normalized bounding box ke UIKit coordinates
func convertBoundingBox(_ box: CGRect, to imageSize: CGSize) -> CGRect {
// Vision uses bottom-left origin, UIKit uses top-left
CGRect(
x: box.origin.x * imageSize.width,
y: (1 - box.origin.y - box.size.height) * imageSize.height,
width: box.size.width * imageSize.width,
height: box.size.height * imageSize.height
)
}
}
Object Detection dengan Custom Core ML Model
swift
// Vision: object detection menggunakan model .mlpackage
import Vision
import CoreML
class ObjectDetector {
private let model: VNCoreMLModel
init() throws {
// Load model (buat via Create ML atau download dari model zoo)
let config = MLModelConfiguration()
config.computeUnits = .cpuAndNeuralEngine
let mlModel = try MyObjectDetector(configuration: config).model
self.model = try VNCoreMLModel(for: mlModel)
}
struct Detection {
let label: String
let confidence: Float
let boundingBox: CGRect
}
func detect(in image: UIImage) async throws -> [Detection] {
guard let cgImage = image.cgImage else { throw VisionError.invalidImage }
return try await withCheckedThrowingContinuation { continuation in
let request = VNCoreMLRequest(model: model) { request, error in
if let error { continuation.resume(throwing: error); return }
let detections = request.results?
.compactMap { result -> Detection? in
guard let obs = result as? VNRecognizedObjectObservation,
let label = obs.labels.first else { return nil }
return Detection(
label: label.identifier,
confidence: label.confidence,
boundingBox: obs.boundingBox
)
}
.filter { $0.confidence > 0.5 } ?? []
continuation.resume(returning: detections)
}
request.imageCropAndScaleOption = .scaleFit // sesuaikan dengan model
let handler = VNImageRequestHandler(cgImage: cgImage)
try? handler.perform([request])
}
}
}
Body Pose Detection
swift
// Vision: deteksi pose tubuh (iOS 14+)
class PoseDetector {
struct BodyPose {
let joints: [VNHumanBodyPoseObservation.JointName: VNRecognizedPoint]
let confidence: Float
}
func detectPose(in image: UIImage) async throws -> [BodyPose] {
guard let cgImage = image.cgImage else { throw VisionError.invalidImage }
return try await withCheckedThrowingContinuation { continuation in
let request = VNDetectHumanBodyPoseRequest { request, error in
if let error { continuation.resume(throwing: error); return }
let poses = request.results?.compactMap { observation -> BodyPose? in
guard let pose = observation as? VNHumanBodyPoseObservation else { return nil }
// Ambil semua joint yang terdeteksi
let allPoints = (try? pose.recognizedPoints(.all)) ?? [:]
return BodyPose(
joints: allPoints,
confidence: pose.confidence
)
} ?? []
continuation.resume(returning: poses)
}
let handler = VNImageRequestHandler(cgImage: cgImage)
try? handler.perform([request])
}
}
// Hitung sudut antar joint (untuk fitness app)
func calculateJointAngle(
joint1: VNRecognizedPoint,
center: VNRecognizedPoint,
joint2: VNRecognizedPoint
) -> Double {
let v1 = CGVector(dx: joint1.x - center.x, dy: joint1.y - center.y)
let v2 = CGVector(dx: joint2.x - center.x, dy: joint2.y - center.y)
let dot = v1.dx * v2.dx + v1.dy * v2.dy
let mag1 = sqrt(v1.dx * v1.dx + v1.dy * v1.dy)
let mag2 = sqrt(v2.dx * v2.dx + v2.dy * v2.dy)
guard mag1 > 0, mag2 > 0 else { return 0 }
return acos(dot / (mag1 * mag2)) * 180 / .pi
}
}
Real-time Video Analysis
swift
// Vision: analisis frame video secara real-time dengan AVCaptureSession
import AVFoundation
import Vision
@MainActor
class LiveVideoAnalyzer: NSObject {
private let captureSession = AVCaptureSession()
private let videoOutput = AVCaptureVideoDataOutput()
private var sequenceRequestHandler = VNSequenceRequestHandler()
// Callback dengan hasil analisis
var onFaceDetected: @MainActor ([VNFaceObservation]) -> Void = { _ in }
private let analysisQueue = DispatchQueue(
label: "com.company.vision.analysis",
qos: .userInitiated
)
func startCapture() throws {
captureSession.beginConfiguration()
captureSession.sessionPreset = .high
guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front),
let input = try? AVCaptureDeviceInput(device: device) else {
throw CaptureError.cameraUnavailable
}
captureSession.addInput(input)
videoOutput.setSampleBufferDelegate(self, queue: analysisQueue)
videoOutput.alwaysDiscardsLateVideoFrames = true // drop frame jika analisis lambat
captureSession.addOutput(videoOutput)
captureSession.commitConfiguration()
captureSession.startRunning()
}
}
extension LiveVideoAnalyzer: AVCaptureVideoDataOutputSampleBufferDelegate {
nonisolated func captureOutput(
_ output: AVCaptureOutput,
didOutput sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection
) {
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
let faceRequest = VNDetectFaceRectanglesRequest()
// VNSequenceRequestHandler lebih efisien untuk video
// karena bisa reuse state antar frame
try? sequenceRequestHandler.perform(
[faceRequest],
on: pixelBuffer,
orientation: .leftMirrored // sesuaikan dengan orientasi kamera
)
let faces = faceRequest.results ?? []
Task { @MainActor in
self.onFaceDetected(faces)
}
}
}