Vision을 이용한 이미지 인식

Vision을 이용한 얼굴인식이나 QRCode 인식에 대해서 살펴보았는데, 사실 이러한 기능들은 정확도면에서 조금 뒤떨어질 수는 있지만 코어 이미지에서도 어느 정도 지원했었던 기능이다. 대신에 Vision은 CoreML과 결합하여 이미지 내의 오브젝트를 인식하고 그 이름을 추출할 수 있는 기능을 제공한다.  (고양이 사진을 보고 ‘고양이’를 알아내는 바로 그 기능이다.)

이번 시간에는 Vision + CoreML을 이용한 이미지 내 사물 인식 기능을 어떻게 구현할 수 있는지 알아보자.

학습모델

이미지 내 사물 인식을 구현하는데 먼저 필요한 것은 이미지로부터 오브젝트를 인식하는 학습을 거쳐 생성된 모델데이터이다. 이 모델데이터들은 애플 개발자 홈페이지에서 다운로드 받을 수 있다. (https://developer.apple.com/kr/machine-learning/build-run-models/) 여기서 InceptionV3를 다운로드 받으면 되고 (그외의 다른 모델을 사용해도 된다.) 실제 프로젝트를 생성한 후에 다운로드 받은 모델데이터를 Xcode 프로젝트로 드래근 & 드롭하면 해당 모델이 설치된다. 

모델 인스턴스 생성

Xcode 프로젝트로 모델을 추가하면 해당 모델에 대한 클래스가 자동으로 생성된다. 해당 모델의 클래스로부터 VNCoreMLModel 인스턴스를 생성하여 사용한다. 실제로 우리는 모델 객체를 직접적으로 조작할 일은 없고 모든 처리는 내부적으로 수행된다. 다만 주의할 것은 학습모델 클래스들의 이니셜라이저는 예외를 던지는 타입이기 때문에 try 문으로 작성되어야 한다는 점이다. 모델 인스턴스를 생성하는 방법은 다음과 같다.

let model = try! VNCoreMLModel(for: MobileNet().model)

CoreML 요청과 그 결과

Vision에서 CoreML 모델을 사용한 이미지 분석 요청은 VNCoreMLRequest 클래스로 표현되며, 이 클래스는 요청 객체를 만들 때 앞서 정의한 VNCoreML 모델 객체를 필요로 한다.  일단 요청 자체는 아래와 같이 만들 수 있다. 결과를 처리하는 부분은 별도의 함수로 넘겨서 뒤에서 살펴보도록 하자.

let request = VNCoreModelRequest(model: model){ request, error in
  processClassifications(for: request, error: error)
}

CoreML 이미지 분석 시 유의할 점은 기계학습은 정사각형 형태의 이미지를 학습하는데, 실제 주어지는 이미지는 제각각의 형태를 가지고 있다는 것이다. 따라서 정사각형이 아닌 이미지를 어떻게 처리할 것인가 하는 방법을 결정해주어야 한다. 가장 나은 결과를 뽑아내기 위해서는 이미지의 가운데에서 정사각형 영역으로 잘라 사용하는 것이다.  이 옵션은 imageCorpAndScaleOption 프로퍼티에 정의되며 그 값은 VNImageCropAndScaleOption에 정의되어 있다. 우리는 그 중 .centerCrop을 사용할 것이다. 

request.imageCropAndScaleOption = .centerCrop

요청을 실행하기

이미지 분석 요청은 얼굴 인식이나 QR코드 인식과는 연산량의 레벨이 다르기 때문에 제법 시간이 많이 소요될 수 있다. 따라서 앱이 그 동안 멈추지 않기 위해서는 백그라운드에서 작업을 수행하도록 해주어야 한다. 만약 명령줄 프로그램이라면 이렇게 동작할 필요는 없다.  기본적으로 이미지 분석이기 때문에 VNImageRequestHandler를 그대로 사용하면 된다.

// 백그라운드에서 실행
DispatchQueue.global(qos: .userInitiated).async {
  let handler = VNImageRequestHandler(cgImage: source, options:[:])
  do {
    try handler.perform([request])
  } catch {
    print("Failed to perform classification: \(error.localizedDescription)")
  }
}

결과를 처리하기

이미지 분석 결과는 VNClassificationObervation 으로, identifier라는 프로퍼티로 그 대상의 이름을 식별할 수 있다. 또한 Confidence 값을 사용하면 0~1 사이에서 어느 정도로 명확하게 해당 물체를 구분하였는지를 알 수 있다. 물론 하나의 이미지에서 추출 가능한 식별자는 여러 개이기 때문에 그 중에서 가장 값이 큰 것 (일반적으로 맨 앞에 온다)을 사용하면 되겠다. 

참고로 VNClassificationObservationVNRectangleObservation을 상속하지 않기 때문에 boundingBox 프로퍼티가 없다. 즉 찾아낸 feature가 이미지의 어느 곳에 위치하는지는 알려주지 않는다.

func processClassifications(for request: VNCoreMLRequest, error : Error?) {

  // 에러가 있는지부터 확인
  guard error == nil else {
    print("ERROR: \(error!.localizedDescription)")
    return
  }

  // 결과 탐지
  guard let results = request.results as? [VNClassificationObservation] else {
  print("Not supported types")
  return
  }

  // 가장 confidence 값이 높은 1개만 추리기
  let maxResult = results.max{ $0.confidence > $1.confidence }
  
  // 결과를 UI에 반영
  DispatchQueue.main.async {
    changeDisplayedText(to: maxResult.identifier)
  }
}