포스트

CoreML (3)

Vision Framework 시작하기

Vision framework는 컴퓨터 비전 작업에 특화되어 있다. 정지 이미지에서의 얼굴 추적, 얼굴 감지, 텍스트 감지, 모양 감지, 바코드 감지 등을 포함한다. 이 섹션에서는 몇 가지 iOS 앱을 만들어보면서 이 기능들을 직접 살펴본다.

  • 이미지에서 사각형 영역을 감지하는 rectangle detector
  • 이미지 내 텍스트 영역을 인식하고 구분하는 text detector
  • 다양한 바코드를 인식하는 barcode recognizer
  • 사진 속 얼굴을 식별하는 face recognizer

이렇게 만드는 게 생각보다 훨씬 간단하다는 걸 확인하게 될 것이다.


Starter 프로젝트

이 섹션의 모든 프로젝트가 출발점으로 삼는 starter 앱이 준비되어 있다. ViewController에는 이미지를 보여줄 imageView, 카메라 버튼(cameraButton)이 outlet으로 연결되어 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class ViewController: UIViewController {

    @IBOutlet weak var imageView: UIImageView!
    @IBOutlet weak var cameraButton: UIBarButtonItem!

    @IBAction func takePicture() {
        guard UIImagePickerController.isSourceTypeAvailable(.camera) else {
            presentPhotoPicker(sourceType: .photoLibrary)
            return
        }

        let photoSourcePicker = UIAlertController()
        let takePhoto = UIAlertAction(title: "Take Photo", style: .default) { [unowned self] _ in
            self.presentPhotoPicker(sourceType: .camera)
        }
        let choosePhoto = UIAlertAction(title: "Choose Photo", style: .default) { [unowned self] _ in
            self.presentPhotoPicker(sourceType: .photoLibrary)
        }

        photoSourcePicker.addAction(takePhoto)
        photoSourcePicker.addAction(choosePhoto)
        photoSourcePicker.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))

        present(photoSourcePicker, animated: true)
    }

    private func presentPhotoPicker(sourceType: UIImagePickerController.SourceType) {
        let picker = UIImagePickerController()
        picker.delegate = self
        picker.sourceType = sourceType
        present(picker, animated: true)
    }
}

extension ViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
        picker.dismiss(animated: true)

        guard let uiImage = info[.originalImage] as? UIImage else {
            fatalError("Error! Could not retrieve image from image picker.")
        }

        imageView.image = uiImage
    }
}

동작 흐름은 이렇다.

  • takePicture()가 카메라 버튼의 액션이다. 먼저 UIImagePickerController.isSourceTypeAvailable(.camera)로 기기에 카메라가 있는지 확인한다. 없으면(시뮬레이터가 대표적인 경우다) 바로 사진 라이브러리로 넘어간다
  • 카메라가 있으면 “Take Photo”/”Choose Photo”/”Cancel” 세 가지 액션이 담긴 UIAlertController를 띄워서 사용자가 소스를 고르게 한다
  • presentPhotoPicker(sourceType:)가 실제로 UIImagePickerController를 생성하고, delegate를 self로, sourceType을 전달받은 값(.camera 또는 .photoLibrary)으로 설정한 뒤 화면에 띄운다
  • 사용자가 사진을 찍거나 선택하면 imagePickerController(_:didFinishPickingMediaWithInfo:) delegate 메서드가 호출된다. info dictionary에서 .originalImage로 원본 이미지를 꺼내서 imageView.image에 대입한다. 이걸 못 꺼내는 경우는 사실상 없다고 보고 fatalError로 처리해뒀다

Vision Framework 동작 원리: Request, Handler, Observation

실제 구현에 들어가기 전에 Vision framework가 어떻게 동작하는지 짚고 넘어간다. 세 가지 타입이 함께 움직인다.

  • Request: Vision한테 뭔가를 감지해달라고 요청하는 객체
  • Handler: 요청이 끝난 뒤 호출되는 completion handler. 요청 결과를 처리한다
  • Observation: 요청이 반환하는 결과. 감지된 대상에 대한 정보를 담고 있다

이미지에서 사각형을 감지하는 코드로 예를 들면 이렇다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var rectangleDetectionRequest: VNDetectRectanglesRequest {
    let request = VNDetectRectanglesRequest(completionHandler: { (request, error) in
        if let detectError = error as NSError? {
            print(detectError)
            return
        } else {
            guard let observations = request.results as? [VNDetectedObjectObservation] else {
                return
            }

            print(observations)
            self.visualizeObservations(observations)
        }
    })
}

rectangleDetectionRequest가 request이고, 타입은 VNDetectRectanglesRequest다. handler는 비동기로 호출되는 completion handler 블록이고, 기대하는 결과는 VNRectangleObservation 인스턴스 배열이다(위 코드는 좀 더 범용적인 VNDetectedObjectObservation으로 캐스팅하고 있다). 이미지에서 사각형을 하나도 못 찾으면 observation이 반환되지 않을 수도 있다.

Vision에는 사각형 말고도 텍스트, 바코드, 얼굴 영역을 감지하는 request class들이 각각 따로 있고, 각 request 타입마다 짝을 이루는 observation 타입(VNRectangleObservation, VNBarcodeObservation 등)이 있다.


Massive View Controller 피하기: type extension으로 분리하기

정지 이미지에서 감지된 사각형 영역을 시각화하는 앱을 만들면서, view controller가 비대해지는 문제를 짚고 넘어간다.

view controller는 원래 view 관련 로직만 다뤄야 하는데, 기능이 하나둘 늘어날 때마다 데이터 파싱 로직, 네트워킹 코드 같은 걸 계속 여기에 쌓다 보면 코드가 방대해지고 유지보수가 어려워진다. 이걸 흔히 massive view controller라고 부른다.

이걸 피하는 가장 쉬운 방법은 type extension을 별도 파일로 분리하는 것이다. view controller에 들어가면 안 되는 로직을 이 확장 파일에 담는다. 여기서는 ViewController+Vision.swift라는 파일을 만들어서, Vision 관련 로직을 전부 이 확장 안에 구현한다.

1
2
3
4
5
6
import UIKit
import Vision

extension ViewController {
    // Vision 관련 로직을 여기에 모아둔다
}

Vision Request 실행하기

performVisionRequest(image:) 메서드

이미지 요청 핸들러를 만들고 Vision 요청을 실행하는 메서드를 만든다.

1
2
3
4
5
func performVisionRequest(image: UIImage) {
    guard let cgImage = image.cgImage else { return }

    let imageRequestHandler = VNImageRequestHandler(cgImage: cgImage, orientation: image.cgOrientation, options: [:])
}

UIImage를 입력으로 받는다. VNImageRequestHandler의 초기화 함수는 CGImage, 이미지 방향, 옵션 dictionary를 받는 형태를 쓴다.

첫 번째 인자인 CGImageUIImage에서 꺼낼 수 있는데, 이 데이터가 없으면 처리를 계속할 수 없으니 guard let으로 없으면 바로 반환한다.

두 번째 인자인 이미지 방향은 CGImagePropertyOrientation 타입이어야 한다. UIImage에도 imageOrientation이라는 방향 프로퍼티가 있지만, 이 값을 그대로 캐스팅하면 안 되는 이유가 있다(아래 UIImage+Extension 참고). 그래서 위 코드는 방금 정의한 cgOrientation 계산 프로퍼티를 대신 사용한다.

세 번째 인자인 옵션은 빈 dictionary([:])로 둔다.


UIImage+Extension: cgOrientation 변환

새 파일 UIImage+Extension.swift를 만든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import UIKit

extension UIImage {
    var cgOrientation: CGImagePropertyOrientation {
        switch imageOrientation {
        case .up : return .up
        case .upMirrored: return .upMirrored
        case .down: return .down // 0th row at bottom, 0th column on right  - 180 deg rotation
        case .downMirrored : return .downMirrored // 0th row at bottom, 0th column on left   - vertical flip
        case .leftMirrored : return .leftMirrored // 0th row on left,   0th column at top
        case .right : return .right // 0th row on right,  0th column at top    - 90 deg CW
        case .rightMirrored : return .rightMirrored // 0th row on right,  0th column on bottom
        case .left : return .left // 0th row on left,   0th column at bottom - 90 deg CCW
        }
    }
}

extension에는 저장 프로퍼티를 넣을 수 없어서 계산 프로퍼티로 만든다. UIImage.Orientation의 각 케이스를 이름이 같은 CGImagePropertyOrientation 케이스로 하나하나 매핑해준다.

여기서 의아할 수 있는 지점은, 결과적으로 .up.up으로, .down.down으로, 이름이 그대로 대응된다는 점이다. 이러면 굳이 switch로 케이스를 일일이 나열할 필요 없이, CGImagePropertyOrientation(rawValue: imageOrientation.rawValue)처럼 raw value를 그대로 캐스팅해도 되지 않을까 싶어진다.

하지만 이게 바로 함정이다. 두 enum은 이름은 같은 케이스들을 갖고 있지만, 그 케이스들이 선언된 순서(그래서 배정되는 raw value)가 서로 다르다. 즉 UIImage.Orientation.down의 raw value와 CGImagePropertyOrientation.down의 raw value가 일치한다는 보장이 없다. 그래서 raw value 기반으로 단순 캐스팅하면, 이름은 다른데 raw value가 같은 엉뚱한 케이스로 잘못 변환될 위험이 있다. 이 switch 문은 그런 사고를 막기 위해, 케이스 이름 기준으로 명시적으로 하나하나 매핑해주는 안전장치인 셈이다.


detectionRequest: 사각형 감지 요청 구현하기

이미지 요청 핸들러는 perform(_:) 인스턴스 메서드로 하나 이상의 Vision 요청을 실행할 수 있다. 이번엔 그 안에 넣을 실제 사각형 감지 요청을 구현한다.


detectionRequest 계산 프로퍼티

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var detectionRequest: VNDetectRectanglesRequest {
    let request = VNDetectRectanglesRequest { (request, error) in
        if let detectError = error as NSError? {
            print(detectError)
            return
        } else {
            guard let observations = request.results as? [VNDetectedObjectObservation] else {
                return
            }
            print(observations)
        }
    }
    return request
}

VNDetectRectanglesRequest 타입의 detectionRequest 프로퍼티를 선언한다. 이 request 타입이 이미지에서 사각형 영역을 찾아준다. Swift의 type extension은 저장 프로퍼티를 정의할 수 없어서, 이것도 계산 프로퍼티로 만든다.

VNDetectRectanglesRequest의 초기화 함수는 요청이 완료됐을 때 호출되는 completion handler 클로저를 받는다. 이 클로저는 완료된 request와, 문제가 있었다면 optional error를 인자로 받는다. error as NSError?로 캐스팅해서 값이 있으면 콘솔에 출력하고 바로 return한다.

에러가 없으면 else 블록에서 결과를 처리한다. request.results는 optional이라 guard let으로 값이 있는지 확인하고, 특정 결과 타입에 관심이 없을 때 쓸 수 있는 범용 타입인 VNDetectedObjectObservation 배열로 캐스팅한다. 캐스팅에 실패하거나 결과가 없으면 return하고, 성공하면 일단 콘솔에 관찰 결과를 출력해본다.

마지막으로 request를 반환해서 계산 프로퍼티를 완성한다.


performVisionRequest 완성하기

이제 performVisionRequest(image:)로 돌아가서, 앞서 만든 imageRequestHandlerdetectionRequest를 실행하는 코드를 이어붙인다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func performVisionRequest(image: UIImage) {
    guard let cgImage = image.cgImage else {
        return
    }
    
    let imageRequestHandler = VNImageRequestHandler(cgImage: cgImage,
                                                    orientation: image.cgOrientation,
                                                    options: [:])
    let requests = [detectionRequest]
    // Send the requests to the request handler.
    DispatchQueue.global(qos: .userInitiated).async {
        do {
            try imageRequestHandler.perform(requests)
        } catch let error as NSError {
            print("Failed to perform image request: \(error)")
            return
        }
    }
}

perform(_:)은 여러 요청을 한 번에 처리할 수 있게 배열을 받는다. 여기선 요청이 하나뿐이라 단일 요소 배열([detectionRequest])을 전달한다. perform(_:) 호출은 에러를 던질 수 있어서 try로 감싸고, catch let error as NSError로 캐스팅해서 어떤 요청이 실패했는지 알 수 있게 메시지에 맥락을 붙여 출력한다.

이미지 분석 요청은 연산 비용이 크다. UI가 멈추지 않도록, perform(_:) 호출을 백그라운드 큐에서 실행한다. 시스템이 제공하는 global concurrent queue를 DispatchQueue.global(qos:)로 가져오는데, 여기선 quality of service를 .userInitiated로 지정했다.


이미지 선택 시 자동 실행되도록 연결하기

마지막으로, 사용자가 이미지를 선택한 직후 performVisionRequest(image:)가 호출되도록 image picker delegate 메서드를 완성한다.

1
2
3
4
5
6
7
8
9
10
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
    picker.dismiss(animated: true)

    guard let uiImage = info[.originalImage] as? UIImage else {
        fatalError("Error! Could not retrieve image from image picker.")
    }

    imageView.image = uiImage
    performVisionRequest(image: uiImage)
}

빌드해서 실행해보고, 사각형 영역이 많은 이미지를 선택해보면, 앱이 하나 이상의 사각형 영역을 감지했을 때 콘솔에 경계 사각형 모서리의 정규화된 좌표가 담긴 observation들이 출력된다.

1
[<VNRectangleObservation: 0x112359880> 70BE7852-A1D1-44CF-A91A-A87810781248 VNDetectRectanglesRequestRevision1 confidence=1.000000 boundingBox=[0.013636, 0.102861, 0.433898, 0.544877]]

감지된 관측값 시각화하기

콘솔에 좌표만 찍어보는 걸로는 부족하니, 감지된 사각형을 실제로 이미지 위에 그려서 보여준다. ViewController+Vision 확장에 visualizeObservations(_:)라는 private 메서드를 새로 만든다.


메인 큐로 위임하기

1
2
3
4
5
6
7
8
9
10
11
private func visualizeObservations(_ observations: [VNDetectedObjectObservation]) {
    DispatchQueue.main.async { [weak self] in
        guard let self = self, let image = self.imageView.image else {
            print("No image to analyze.")
            return
        }

        let imageSize = image.size
        // ...
    }
}

이 메서드는 UI를 다루니까 메인 큐로 비동기 위임하는 게 필수다. DispatchQueue.main.async로 감싸고, 클로저 안에서 self의 프로퍼티나 메서드에 접근하려면 self를 명시적으로 캡처해야 한다.

먼저 imageView.image로 분석할 이미지가 실제로 있는지 guard let으로 확인한다. 없으면 분석을 계속할 이유가 없으니 이유를 출력하고 return한다. 그다음 CoreGraphics 연산에 쓸 이미지 크기(imageSize)를 image.size로 꺼내둔다.


좌표계 변환이 필요한 이유

Vision이 반환하는 observation의 좌표는 UIKit 좌표계와 다르다.

  • Vision은 Quartz 2D 좌표계를 쓰는데, 이건 UIKit 좌표계와 비교했을 때 X축 기준으로 위아래가 뒤집혀 있다
  • 게다가 좌표값은 0~1 사이로 정규화(normalized)되어 있어서, 실제 이미지 픽셀 크기에 맞게 다시 스케일링해야 한다

그래서 감지된 경계 사각형(bounding box)을 실제로 화면에 그리려면, 각 사각형마다 다음 변환을 순서대로 적용해야 한다.

  1. X축을 기준으로 뒤집는다
  2. 사각형의 높이만큼 Y축 방향으로 이동시켜서 위치를 다시 맞춘다
  3. 이미지 크기에 맞게 스케일링한다

CGAffineTransform 구성하기

1
2
var transform = CGAffineTransform.identity.scaledBy(x: 1, y: -1).translatedBy(x: 0, y: -imageSize.height)
transform = transform.scaledBy(x: imageSize.width, y: imageSize.height)

이 변환들을 하나씩 짚어보면 이렇다.

  • CGAffineTransform.identity로 시작해서 아무 변형도 없는 기본 상태를 만든다
  • scaledBy(x: 1, y: -1)로 x축은 그대로 두고 y축에 -1을 곱해서, 뒤집힌 좌표계를 UIKit 방향으로 되돌린다
  • translatedBy(x: 0, y: -imageSize.height)로 y축 방향으로 이미지 높이만큼 이동시켜서, 뒤집히면서 밀려난 위치를 원래 자리로 되돌린다
  • scaledBy(x: imageSize.width, y: imageSize.height)로 정규화된(0~1) 좌표를 실제 이미지의 픽셀 크기에 맞게 스케일링한다

이 변환들을 순서대로 적용하고 나면, Vision이 반환한 observation의 좌표가 UIKit 좌표계로 정확히 변환되고 이미지 실제 크기에 맞게 스케일링된 상태가 된다. 이 transform을 각 경계 사각형에 적용하면 실제 화면 위에 올바른 위치와 크기로 그릴 수 있게 된다.


경계 사각형 그리기

좌표 변환이 끝났으니, 이제 이 transform을 이용해서 실제로 감지된 사각형을 이미지 위에 그린다.


비트맵 컨텍스트 생성 및 원본 이미지 그리기

1
2
3
4
UIGraphicsBeginImageContextWithOptions(imageSize, true, 0.0)
let context = UIGraphicsGetCurrentContext()

image.draw(in: CGRect(origin: .zero, size: imageSize))

UIGraphicsBeginImageContextWithOptions(_:_:_:)로 이미지 크기만큼의 비트맵 기반 그래픽 컨텍스트를 만든다. 두 번째 인자(opaque)는 true로 줘서 불투명하게 하고, 세 번째 인자(scale)는 0.0으로 줘서 별도 스케일링 없이 처리한다. UIGraphicsGetCurrentContext()로 현재 컨텍스트를 가져오고, 그 컨텍스트 안에 원본 이미지를 전체 크기로 그려 넣는다.


경계 사각형 스타일 지정하기

1
2
3
4
5
6
context.saveGState()

context.setLineWidth(8.0)
context.setLineJoin(CGLineJoin.round)
context.setStrokeColor(UIColor.red.cgColor)
context.setFillColor(red: 1, green: 0, blue: 0, alpha: 0.3)

선이나 획, 색상 같은 그리기 속성을 바꾸기 전에 saveGState()로 현재 그래픽 상태를 저장해둔다. 나중에 그리기가 끝나면 이 상태로 복원할 것이다.

선 너비는 8로 두껍게, 선 연결 부위는 .round(둥글게)로 설정한다. 획(stroke) 색상은 빨간색(UIColor.red.cgColor)으로, 채우기(fill) 색상도 빨간색으로 하되 RGB 컴포넌트를 직접 지정하는 이니셜라이저로 만들어서 alpha를 0.3으로 줘서 살짝 투명하게 만든다.


각 관측값에 변환 적용하고 경로 그리기

1
2
3
4
5
6
7
observations.forEach({ observation in
    let observationBounds = observation.boundingBox.applying(transform)
    context?.addRect(observationBounds)
})

context.drawPath(using: CGPathDrawingMode.fillStroke)
context.restoreGState()

각 observation을 순회하면서, 그 boundingBox(정규화된 CGRect)에 앞서 만든 transform을 적용해서 실제 이미지 좌표계의 사각형으로 변환한다. 이 사각형을 컨텍스트에 경로로 추가하고, 순회가 끝나면 drawPath(using: .fillStroke)로 한 번에 채우기와 획을 모두 그린다. 그리기가 끝나면 restoreGState()로 그래픽 상태를 원래대로 되돌린다.


결과 이미지 가져와서 표시하기

1
2
3
4
5
let drawnImage = UIGraphicsGetImageFromCurrentImageContext()

UIGraphicsEndImageContext()

self.imageView.image = drawnImage

UIGraphicsGetImageFromCurrentImageContext()로 지금까지 그린 결과를 하나의 UIImage로 가져오고, UIGraphicsEndImageContext()로 앞서 만들었던 비트맵 컨텍스트를 정리한다. 마지막으로 이 결과 이미지를 imageView.image에 대입하면, 원본 이미지 위에 감지된 사각형들이 겹쳐 그려진 화면이 표시된다.


detectionRequest에서 visualizeObservations 호출하기

이제 detectionRequest의 completion handler에서 콘솔에 출력만 하던 부분을, 실제로 방금 만든 visualizeObservations(_:)를 호출하도록 바꾼다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var detectionRequest: VNDetectRectanglesRequest {
    let request = VNDetectRectanglesRequest( completionHandler: { (request, error) in
        if let detectError = error as NSError? {
            print(detectError)
            return
        } else {
            guard let observations = request.results as? [VNDetectedObjectObservation] else {
                return
            }
            
            print(observations)
            self.visualizeObservations(observations)
        }
    })
    
    request.maximumObservations = 0
    request.minimumConfidence = 0.5
    request.minimumAspectRatio = 0.4
    
    return request
}

클로저 안에서 self의 메서드를 호출하는 것이니 self를 명시적으로 붙인다. 그리고 request 자체의 속성도 몇 가지 설정했다.

  • maximumObservations = 0: 감지 가능한 사각형 수에 제한을 두지 않고, Vision이 찾을 수 있는 만큼 전부 찾도록 한다
  • minimumConfidence = 0.5: 신뢰도가 50% 이상인 결과만 받는다
  • minimumAspectRatio = 0.4: 감지할 사각형의 최소 종횡비를 지정한다

실행 결과

앱을 실행해서 같은 사진으로 테스트해보면, 이번엔 콘솔 로그가 아니라 실제 화면에서 감지된 사각형들이 빨간 테두리와 반투명한 채우기로 강조 표시되는 걸 볼 수 있다.

이번 구현에 타이핑이 꽤 많이 들어갔지만, 여기서 만든 코드 기반은 재사용성이 높다. 몇 가지만 바꾸면 같은 구조로 바코드, 텍스트 영역, 얼굴을 감지하는 데모도 만들 수 있다.


요청 타입만 바꿔서 다른 감지 기능 만들기

지금까지 만든 사각형 검출기 코드 기반은 그대로 두고, request 타입만 바꾸면 텍스트, 얼굴, 바코드 감지로 손쉽게 전환할 수 있다.


텍스트 영역 감지: VNDetectTextRectanglesRequest

ViewController+Vision.swiftdetectionRequest 프로퍼티에서, request 타입을 VNDetectRectanglesRequest에서 VNDetectTextRectanglesRequest로 바꾼다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var textDetectionRequest: VNDetectTextRectanglesRequest {
    let request = VNDetectTextRectanglesRequest( completionHandler: { (request, error) in
        if let detectError = error as NSError? {
            print(detectError)
            return
        } else {
            guard let observations = request.results as? [VNDetectedObjectObservation] else {
                return
            }
            
            print(observations)
            self.visualizeObservations(observations)
        }
    })
    
    request.reportCharacterBoxes = true
    
    return request
}

VNDetectRectanglesRequest에만 있던 maximumObservations, minimumConfidence, minimumAspectRatio 속성은 VNDetectTextRectanglesRequest에는 없으니 제거한다. 대신 reportCharacterBoxestrue로 설정한다. 기본값은 false인데, 이 값을 켜면 문자 단위의 경계 상자까지 감지하도록 요청할 수 있다.

이 상태로 실행해서 텍스트가 담긴 이미지를 선택해보면, 텍스트 영역이 강조 표시되어 나타난다.


얼굴 감지: VNDetectFaceRectanglesRequest

같은 방식으로 request 타입을 VNDetectFaceRectanglesRequest로 바꾼다. 이 타입에는 reportCharacterBoxes가 없으니 그 줄은 지운다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var faceDetectionRequest: VNDetectFaceRectanglesRequest {
    let request = VNDetectFaceRectanglesRequest( completionHandler: { (request, error) in
        if let detectError = error as NSError? {
            print(detectError)
            return
        } else {
            guard let observations = request.results as? [VNDetectedObjectObservation] else {
                return
            }
            
            print(observations)
            self.visualizeObservations(observations)
        }
    })
    
    return request
}

실행해서 얼굴이 담긴 이미지를 선택하면 얼굴 영역이 정확히 감지되어 표시된다.


바코드 감지: VNDetectBarcodesRequest

바코드 감지도 마찬가지로 request 타입만 VNDetectBarcodesRequest로 바꾸면 된다. 나머지 코드는 전혀 손댈 필요가 없다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var barcodesDetectionRequest: VNDetectBarcodesRequest {
    let request = VNDetectBarcodesRequest( completionHandler: { (request, error) in
        if let detectError = error as NSError? {
            print(detectError)
            return
        } else {
            guard let observations = request.results as? [VNDetectedObjectObservation] else {
                return
            }
            
            print(observations)
            self.visualizeObservations(observations)
        }
    })
    
    return request
}

바코드가 담긴 이미지로 실행해보면, 바코드 영역이 올바르게 감지되고 강조 표시된다.


연습: 여러 감지 기능을 한 번에 실행하기

사각형, 얼굴, 바코드, 텍스트 영역을 동시에 감지하도록 프로젝트를 확장해볼 수 있다.

지금처럼 각 요청을 계산 프로퍼티로 따로 정의해두고, 필요한 만큼 요청 인스턴스를 만든 뒤, 이걸 전부 담은 배열을 imageRequestHandler.perform(_:)에 한꺼번에 전달하면 된다. perform(_:)이 애초에 여러 요청을 배열로 받는 형태였으니, 지금까지 하나만 넣던 그 배열에 필요한 요청들을 전부 추가하기만 하면 된다.

1
let requests = [rectangleDetectionRequest, textDetectionRequest, faceDetectionRequest, barcodesDetectionRequest]

Vision framework가 제공하는 이미지 분석 기능은 이렇게 request 타입 하나만 바꾸는 것만으로도 완전히 다른 기능으로 전환될 만큼 일관된 인터페이스를 갖고 있다. 덕분에 정교한 컴퓨터 비전 기능을 앱에 큰 부담 없이 통합할 수 있다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.