iOS Vision Framework: 이미지 분석과 ML 모델 통합

iOS

Vision FrameworkiOSCore MLOCR이미지 분석

이 글은 누구를 위한 것인가

  • 카메라에서 실시간으로 텍스트, 바코드, 얼굴을 인식하려는 팀
  • Core ML 모델을 앱에 통합하려는 iOS 개발자
  • Vision API로 이미지 분석 기능을 구현하려는 팀

들어가며

Vision Framework는 이미지 분석을 위한 Apple의 고수준 API다. 얼굴 감지, 텍스트 인식(OCR), 바코드 스캔, Core ML 모델 추론을 일관된 인터페이스로 제공한다. AVFoundation과 결합해 카메라 스트림을 실시간으로 분석한다.

이 글은 bluefoxdev.kr의 iOS Vision Framework Core ML 가이드 를 참고하여 작성했습니다.


1. Vision Framework 주요 기능

[Vision Request 유형]

텍스트 인식:
  VNRecognizeTextRequest: OCR (한국어 지원)
  정확도: accurate/fast 선택
  
얼굴 감지:
  VNDetectFaceRectanglesRequest: 얼굴 위치
  VNDetectFaceLandmarksRequest: 눈/코/입 랜드마크
  VNDetectFaceQualityRequest: 얼굴 품질 점수

바코드/QR:
  VNDetectBarcodesRequest: QR, EAN, Code128 등

이미지 분류:
  VNClassifyImageRequest: 1000개 카테고리
  VNCoreMLRequest: 커스텀 Core ML 모델

텍스트 위치:
  VNDetectTextRectanglesRequest: 텍스트 영역만 (빠름)

[처리 흐름]
  이미지/카메라 프레임 → VNImageRequestHandler
  → [VNRequest 배열] → 결과 (VNObservation)
  
[성능 팁]
  여러 요청을 하나의 Handler로 묶어 처리
  GPU 활용: preferBackgroundProcessing = false
  카메라: 불필요한 프레임 스킵

2. Vision Framework 구현

import Vision
import AVFoundation
import SwiftUI

// 텍스트 인식 (OCR)
class TextRecognizer {
    func recognizeText(in cgImage: CGImage, completion: @escaping ([String]) -> Void) {
        let request = VNRecognizeTextRequest { request, error in
            guard error == nil else { return }
            let texts = (request.results as? [VNRecognizedTextObservation])?.compactMap {
                $0.topCandidates(1).first?.string
            } ?? []
            DispatchQueue.main.async { completion(texts) }
        }
        
        request.recognitionLevel = .accurate
        request.recognitionLanguages = ["ko-KR", "en-US"]
        request.usesLanguageCorrection = true
        
        let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
        try? handler.perform([request])
    }
}

// 바코드/QR 스캔 (실시간 카메라)
class BarcodeScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
    private var captureSession: AVCaptureSession?
    var onBarcodeDetected: ((String) -> Void)?
    
    func startScanning() {
        let session = AVCaptureSession()
        guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back),
              let input = try? AVCaptureDeviceInput(device: device) else { return }
        
        session.addInput(input)
        
        let output = AVCaptureVideoDataOutput()
        output.setSampleBufferDelegate(self, queue: DispatchQueue(label: "barcodeQueue"))
        session.addOutput(output)
        
        captureSession = session
        session.startRunning()
    }
    
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
        
        let request = VNDetectBarcodesRequest { [weak self] request, _ in
            guard let observation = (request.results as? [VNBarcodeObservation])?.first,
                  let payload = observation.payloadStringValue else { return }
            DispatchQueue.main.async { self?.onBarcodeDetected?(payload) }
        }
        
        request.symbologies = [.qr, .ean13, .ean8, .code128, .dataMatrix]
        try? VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [:]).perform([request])
    }
}

// Core ML 이미지 분류
class ImageClassifier {
    private var model: VNCoreMLModel?
    
    init() {
        // Xcode에서 .mlmodel 추가 후 자동 생성된 클래스 사용
        // let mlModel = try? MobileNetV2(configuration: MLModelConfiguration()).model
        // model = try? VNCoreMLModel(for: mlModel!)
    }
    
    func classify(image: CIImage, completion: @escaping (String, Double) -> Void) {
        guard let model = model else { return }
        
        let request = VNCoreMLRequest(model: model) { request, _ in
            guard let results = request.results as? [VNClassificationObservation],
                  let top = results.first else { return }
            DispatchQueue.main.async { completion(top.identifier, Double(top.confidence)) }
        }
        
        request.imageCropAndScaleOption = .centerCrop
        try? VNImageRequestHandler(ciImage: image, options: [:]).perform([request])
    }
}

// 얼굴 감지 + 랜드마크 (SwiftUI)
struct FaceDetectionView: View {
    @State private var detectedFaces: [VNFaceObservation] = []
    let image: UIImage
    
    var body: some View {
        ZStack {
            Image(uiImage: image).resizable().aspectRatio(contentMode: .fit)
            
            GeometryReader { geo in
                ForEach(detectedFaces.indices, id: \.self) { i in
                    let face = detectedFaces[i]
                    let rect = VNImageRectForNormalizedRect(
                        face.boundingBox,
                        Int(geo.size.width), Int(geo.size.height)
                    )
                    Rectangle()
                        .stroke(Color.green, lineWidth: 2)
                        .frame(width: rect.width, height: rect.height)
                        .offset(x: rect.minX, y: geo.size.height - rect.maxY)
                }
            }
        }
        .onAppear { detectFaces() }
    }
    
    private func detectFaces() {
        guard let cgImage = image.cgImage else { return }
        let request = VNDetectFaceLandmarksRequest { req, _ in
            DispatchQueue.main.async {
                detectedFaces = (req.results as? [VNFaceObservation]) ?? []
            }
        }
        try? VNImageRequestHandler(cgImage: cgImage, options: [:]).perform([request])
    }
}

마무리

Vision Framework의 핵심은 VNImageRequestHandler에 여러 요청을 한 번에 묶어 처리하는 것이다. OCR은 recognitionLanguages"ko-KR"을 추가해야 한국어를 인식하고, 실시간 카메라 처리는 AVCaptureVideoDataOutput의 샘플 버퍼를 Vision 요청에 직접 전달한다. Core ML 모델은 .mlmodel 파일을 Xcode에 추가하면 자동으로 Swift 클래스가 생성된다.