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)
        }
    }
}